Double public key (DPK)¶
A double public key bundles a wallet's view and spend pubkeys for a single sub-address into one serialisable object. Addresses in Navio are bech32m-encoded DPKs — not script hashes, not single pubkeys.
Definition¶
- \(V\) — the sub-address's view pubkey, \(V_{a,i} = v \cdot S_{a,i}\). Used by the receiver to scan for incoming outputs.
- \(S\) — the sub-address's spend pubkey, \(S_{a,i} = S_{\text{master}} + \text{tweak}(a, i) \cdot G\). Used by the chain to verify the stealth spending signature.
See Key derivation for how \(V\) and \(S\) are produced from the master seed.
Byte encoding¶
Concatenated compressed \(G_1\) points:
Bech32m address encoding¶
Navio wraps the 96-byte DPK in bech32m for human-readable addresses:
With:
- HRP —
nav(mainnet),tnv(testnet),snv(signet),rnav(regtest). Defined insrc/blsct/key_io.hunderblsct::bech32_hrp. - Witness version — not used. Unlike Bitcoin SegWit bech32 encodings, Navio's
bech32_moddoes not prepend a witness-version byte. The full 96-byte DPK is expanded into 154 × 5-bit groups and placed directly in the data part. - Data part — 154 characters encoding the 96-byte DPK (5-bit groups).
- Checksum — bech32m (BIP-350), with a modified polynomial generator suited to the 154-character payload (see
bech32_mod_gen_poly.md).
A typical testnet address:
Mainnet addresses start with nav1…, signet with snv1…, regtest with rnav1….
bech32_mod¶
Navio uses a modified bech32m reference implementation (src/blsct/bech32_mod.cpp in navio-core) to handle the larger-than-Bitcoin-witness payload size (96 bytes vs. Bitcoin's 20 or 32). The polynomial generator used to compute the checksum is documented in src/blsct/bech32_mod_gen_poly.md.
Validation¶
An address is valid iff:
- bech32m checksum verifies.
- HRP matches the target network.
- Decoded payload is exactly 96 bytes.
- Both 48-byte halves deserialise as valid \(G_1\) points (check subgroup and curve membership).
Use validateaddress or KeyManager.validateAddress.
Browser fallback note¶
navio-blsct on Node.js produces full bech32m addresses. On browser WASM, certain paths may currently return hex-encoded DPKs rather than bech32m strings. This is a known limitation tracked in the library; see the SDK note.
On-chain usage¶
- Outputs never store the DPK directly. Instead they store the derived ephemeral key \(R\) and the stealth spend pubkey \(S' = S + \text{HASH}(rV) \cdot G\). The DPK is the receiver-side identifier only.
- Wallets store the DPK in the
sub_addressestable with(account, index)→ DPK. - Explorers cannot display the DPK on output pages — it is mathematically impossible to recover. An output contains only \(R\) and \(S'\). Reversing \(S'\) back to \((V, S)\) requires the recipient's view private key \(v\) to compute \(\text{HASH}(vR)\) and subtract it from \(S'\). Without \(v\), no observer (explorer, chain analyst, validator) can derive the DPK from chain data. The DPK is recoverable only by the wallet holder; it surfaces in the UI only when a user copies their own receive address.
Creating DPKs programmatically¶
import { KeyManager, BlsctChain, setChain } from 'navio-sdk';
setChain(BlsctChain.Testnet);
const keyManager = // ... loaded or created from seed
const dpk = keyManager.getSubAddress({ account: 0, address: 0 });
console.log(dpk.toHex()); // 192 hex chars
const address = keyManager.getSubAddressBech32m(
{ account: 0, address: 0 },
'testnet'
);
console.log(address); // tnv1…
See the navio-blsct library → keys for the low-level API.