Platform Addresses

Dash Platform has its own address system, independent of the legacy Base58Check addresses used on the Core chain. Platform addresses use Bech32m encoding (BIP-350), following the specification in DIP-0018. There are three address types: two transparent and one shielded.

If you are coming from Bitcoin or Dash Core, the key mental shift is this: platform addresses are not derived from a single private key via a single algorithm. They are typed containers for a hash or a public key, unified under a single encoding scheme with a shared human-readable prefix.

The Three Address Types

TypeInner DataBech32m Type BytePrefix (mainnet)Prefix (testnet)
P2PKH20-byte pubkey hash0xb0dash1k...tdash1k...
P2SH20-byte script hash0x80dash1s...tdash1s...
Orchard43-byte shielded address0x10dash1z...tdash1z...

The type byte is the first byte of the Bech32m data payload. It determines how the rest of the payload is interpreted. The type bytes were chosen so that the first character after dash1 (or tdash1) is a memorable letter: k for keys (P2PKH), s for scripts (P2SH), z for zero-knowledge (Orchard).

PlatformAddress

The transparent address type is defined in packages/rs-dpp/src/address_funds/platform_address.rs:

#![allow(unused)]
fn main() {
pub enum PlatformAddress {
    P2pkh([u8; 20]),
    P2sh([u8; 20]),
}
}

Two variants, both wrapping a 20-byte hash. A P2pkh address contains a Hash160(compressed_pubkey), just like on Core. A P2sh address contains a Hash160(redeem_script), supporting standard multisig scripts.

Bech32m Encoding

The human-readable part (HRP) depends on the network:

#![allow(unused)]
fn main() {
const PLATFORM_HRP_MAINNET: &str = "dash";
const PLATFORM_HRP_TESTNET: &str = "tdash";  // also devnet, regtest
}

Encoding produces addresses like:

  • dash1krma5z3ttj75la4m93xcndna9ullamq9y5e9n5rs (P2PKH, mainnet)
  • tdash1sppl5xpu70aka8nacc4kj2htflydspzkc8jtru5 (P2SH, testnet)

The wire format is type_byte || hash -- 21 bytes total, then Bech32m-encoded with the appropriate HRP.

Storage vs. Bech32m: Two Byte Schemes

There is an important distinction between how addresses are encoded for users and how they are serialized for storage. The Bech32m type bytes (0xb0, 0x80) are not the same as the bincode variant indices (0x00, 0x01) used in GroveDB keys:

ContextP2PKH byteP2SH byte
Bech32m (user-facing)0xb00x80
Bincode (storage/wire)0x000x01

This matters when you encounter raw bytes. If the leading byte is 0xb0 or 0x80, you are looking at a Bech32m payload. If it is 0x00 or 0x01, it is bincode-serialized. The to_bytes() method produces bincode format; to_bech32m_string() produces the user-facing string.

Conversion to Core Addresses

PlatformAddress can be converted to and from dashcore::Address, the type used by the Core chain RPC client. This allows the platform layer to interoperate with Core's address format when needed -- for example, when processing withdrawals that ultimately create Core chain transactions.

OrchardAddress

The shielded address type is defined in packages/rs-dpp/src/address_funds/orchard_address.rs:

#![allow(unused)]
fn main() {
pub struct OrchardAddress(grovedb_commitment_tree::PaymentAddress);
}

It wraps the Orchard protocol's native PaymentAddress, which consists of two components:

ComponentSizePurpose
Diversifier11 bytesEntropy for deriving unlinkable addresses from a single spending key
pk_d32 bytesDiversified transmission key (Pallas curve point)

Total: 43 bytes. The Bech32m payload is 0x10 || diversifier || pk_d -- 44 bytes.

Key Constants

#![allow(unused)]
fn main() {
const ORCHARD_DIVERSIFIER_SIZE: usize = 11;
const ORCHARD_PKD_SIZE: usize = 32;
const ORCHARD_ADDRESS_SIZE: usize = 43;
const ORCHARD_TYPE: u8 = 0x10;
}

Diversifiers and Unlinkability

A single Orchard spending key can derive an unlimited number of addresses by varying the diversifier. Each address looks completely unrelated to any other address derived from the same key. Only the holder of the corresponding Incoming Viewing Key (IVK) can link them.

This is the fundamental privacy property: a merchant can give every customer a unique address, and no observer can determine that all those addresses belong to the same wallet. The diversifier is the mechanism that makes this possible.

Differences from Zcash Encoding

Dash's OrchardAddress uses the same 43-byte raw format as Zcash Orchard addresses (identical diversifier + pk_d structure). But the Bech32m encoding is Dash-specific:

  • No F4Jumble: Zcash applies an F4Jumble permutation before encoding; Dash does not.
  • No Unified Address wrapper: Zcash wraps Orchard addresses in a Unified Address (UA) envelope with typecodes and length prefixes; Dash uses a simple type-byte scheme.
  • Different HRP: Zcash uses u1; Dash uses dash/tdash.

The result is simpler, shorter addresses that are not interoperable with Zcash wallets.

Converting to Orchard's Native Type

The builder functions that construct shielded transactions need the Orchard library's native PaymentAddress, not our wrapper. Conversion is straightforward:

#![allow(unused)]
fn main() {
impl From<&OrchardAddress> for PaymentAddress {
    fn from(address: &OrchardAddress) -> Self {
        *address.inner()
    }
}
}

This is used in the shielded builder module (packages/rs-dpp/src/shielded/builder/) when adding outputs to an Orchard bundle.

Address Witnesses

When a transparent address is used as an input (for example, funding a shield transition), the sender must prove ownership. This is done through an AddressWitness, defined in packages/rs-dpp/src/address_funds/witness.rs:

#![allow(unused)]
fn main() {
pub enum AddressWitness {
    P2pkh {
        signature: BinaryData,
    },
    P2sh {
        signatures: Vec<BinaryData>,
        redeem_script: BinaryData,
    },
}
}

P2PKH Witnesses

A P2PKH witness contains a single 65-byte recoverable ECDSA signature. The public key is recovered from the signature rather than transmitted alongside it -- saving 33 bytes per witness. Verification recovers the public key, hashes it with Hash160, and checks the result against the address hash.

P2SH Witnesses

A P2SH witness contains the signatures and the redeem script. Only standard bare multisig scripts are supported:

OP_M <pubkey1> <pubkey2> ... <pubkeyN> OP_N OP_CHECKMULTISIG

Constraints:

  • Maximum 17 signature entries (MAX_P2SH_SIGNATURES) -- 16 keys plus 1 dummy for the CHECKMULTISIG bug
  • Only compressed public keys (33 bytes)
  • No timelocks, hash puzzles, or custom scripts
  • The redeem script must hash to the address: Hash160(script) == address_hash

Verification Cost Tracking

Every witness verification has a measurable cost. The AddressWitnessVerificationOperations struct tracks the work:

#![allow(unused)]
fn main() {
pub struct AddressWitnessVerificationOperations {
    pub ecdsa_signature_verifications: u16,
    pub message_hash_count: u16,
    pub pubkey_hash_verifications: u16,
    pub script_hash_verifications: u16,
    pub signable_bytes_len: usize,
}
}

These counts feed into the fee system. A P2PKH witness costs one ECDSA verification. An M-of-N multisig costs N verifications (all public keys are checked against all signatures, per Bitcoin's CHECKMULTISIG semantics).

How Addresses Are Used in Shielded Transitions

Each of the five shielded transition types uses addresses differently:

Shield (Transparent to Shielded)

The sender provides one or more PlatformAddress inputs, each with a nonce and a maximum contribution amount. An AddressWitness proves ownership of each input. The funds enter the shielded pool and are received by an OrchardAddress embedded in the Orchard bundle's encrypted outputs.

#![allow(unused)]
fn main() {
pub struct ShieldTransitionV0 {
    pub inputs: BTreeMap<PlatformAddress, (AddressNonce, Credits)>,
    pub input_witnesses: Vec<AddressWitness>,
    pub actions: Vec<SerializedAction>,  // contains encrypted OrchardAddress recipient
    pub amount: u64,
    // ...
}
}

Shielded Transfer (Shielded to Shielded)

No transparent addresses are involved. The sender spends notes from the shielded pool and creates new notes for the recipient's OrchardAddress and a change OrchardAddress. Both addresses are hidden inside the Orchard bundle -- only the respective recipients can decrypt them.

#![allow(unused)]
fn main() {
pub struct ShieldedTransferTransitionV0 {
    pub actions: Vec<SerializedAction>,  // spend + output pairs
    pub value_balance: u64,             // fee extracted from pool
    // ...
}
}

Unshield (Shielded to Transparent)

The sender spends shielded notes and sends the funds to a PlatformAddress visible on-chain. The transparent output address is explicitly included in the transition and bound into the Orchard sighash to prevent substitution.

#![allow(unused)]
fn main() {
pub struct UnshieldTransitionV0 {
    pub output_address: PlatformAddress,  // visible transparent recipient
    pub unshielding_amount: u64,
    pub actions: Vec<SerializedAction>,   // spend + change output
    // ...
}
}

Shielded Withdrawal (Shielded to Core Chain)

Similar to unshield, but the destination is a Core chain script rather than a platform address. The CoreScript holds a raw P2PKH or P2SH script that will be used in a Core chain withdrawal transaction.

#![allow(unused)]
fn main() {
pub struct ShieldedWithdrawalTransitionV0 {
    pub output_script: CoreScript,  // Core chain P2PKH or P2SH
    pub core_fee_per_byte: u32,
    pub pooling: Pooling,
    // ...
}
}

Shield From Asset Lock (Core Chain to Shielded)

An asset lock proof from the Core chain funds the shielded pool directly. The recipient is an OrchardAddress inside the Orchard bundle. No PlatformAddress inputs are needed -- the asset lock proof substitutes for them.

The transition also carries an optional surplus_output: Option<PlatformAddress>. When the consumed asset lock exceeds shield_amount + pool_fee, the leftover surplus is credited to this platform address; if it is unset, the surplus folds into the fee pools (bounded by shielded_implicit_fee_cap). See Entry-Transition Fees. Unlike the transparent recipients of Unshield/ShieldedWithdrawal (which are bound through the Orchard sighash extra_data above), surplus_output is bound through the state transition's own platform_signable signature -- it sits before the signature field, so it is part of the signed payload and cannot be substituted or truncated after signing. The Orchard extra_data therefore does not carry it; what it binds instead is the asset lock itself (see below).

The Platform Sighash

When transparent fields need to be bound to an Orchard bundle's proof, the platform uses a custom sighash computation defined in packages/rs-dpp/src/shielded/mod.rs:

#![allow(unused)]
fn main() {
const SIGHASH_DOMAIN: &[u8] = b"DashPlatformSighash";

pub fn compute_platform_sighash(
    bundle_commitment: &[u8; 32],
    extra_data: &[u8],
) -> [u8; 32] {
    let mut hasher = Sha256::new();
    hasher.update(SIGHASH_DOMAIN);
    hasher.update(bundle_commitment);
    hasher.update(extra_data);
    hasher.finalize().into()
}
}

The bundle_commitment is a BLAKE2b-256 hash of the Orchard bundle (per ZIP-244). The extra_data binds transparent fields:

Transitionextra_data
Shield0x84 || SHA-256(input addresses)
Shield From Identity0x85 || identity_id
Shielded Transferempty
Unshieldoutput_address.to_bytes() || amount.to_le_bytes()
Shielded Withdrawaloutput_script.as_bytes()
Shield From Asset Lock0x86 || asset lock identifier

This binding is critical for security. Without it, an attacker who intercepts an unshield transition could substitute the output_address while reusing the valid Orchard proof and signatures. The sighash ensures the Orchard bundle's spend authorization signatures commit to the specific transparent recipient.

The three transitions that only create notes bind something for a different reason. Their bundles have no spends, so they carry no anchor of their own: the proof and the binding signature verify wherever the bundle is submitted. Left unbound, the authorized bundle is a self-contained object anybody could lift out of the mempool and wrap in a transition of their own, paid with their own credits, and any bundle ever published could be replayed. The copier gains nothing — they pay the full amount to the original recipient — but the copy lands a second note with the same commitment and the same nullifier in the credit pool, so only one of the two can ever be spent.

Each of these bundles therefore binds a kind tag and its owner, the thing that funds it and that a third party cannot authorize:

  • Shield: the SHA-256 of its input addresses, each in its 21-byte encoding (PlatformAddress::to_bytes), in the order the transition serializes its inputs. The nonces and contributed amounts are not included: the owner says who funds the bundle, not which transition carries it.
  • Shield From Identity: the identity id.
  • Shield From Asset Lock: the asset lock identifier, the double SHA-256 of the locked 36-byte outpoint (the same value an identity created from that lock would get as its id; the tag keeps the two apart). Because a successful shield consumes the whole lock, a bundle bound to it can land at most once, even when its own sender resubmits it. Binding only the transaction id would not do this, and would let the holder of another credit output of the same lock transaction lift the bundle.

The tags 0x84, 0x85 and 0x86 sit above the state transition type range, next to the token pools' outputs-only tags 0x80–0x83. The binding starts at protocol version 14; at earlier protocol versions Shield and Shield From Asset Lock bind nothing, and a client building for one of those versions must bind nothing too. Both sides read the choice from the same version field (dpp.methods.credit_pool_bundle_binding), so a client that builds with the network's protocol version produces what that network verifies.

Shield From Asset Lock also changes its transition version at protocol version 14: version 1 carries the bound bundle and is the only version 14 admits, while version 0, whose bundle binds nothing, is admitted only up to 13. A version 0 presented at 14, such as one still waiting in the mempool when 14 activates, is refused on its version byte when the transition is decoded, before any proof is verified: nothing is charged and its asset lock stays unspent, so its sender can resubmit it as version 1 from the same lock. Checking its unbound bundle against the bound preimage instead would fail the proof and burn the proof-failure penalty from an honest lock.

The binding does not prevent Faerie Gold. The rho of an outputs-only note is the nullifier of its bundle's dummy spend, so a sender who builds and signs a fresh bundle reusing the same dummy spend note and rseed gets the same commitment and the same nullifier whatever the preimage binds, and a recipient that counts deposits by commitment credits two payments where only one can be spent. Nor does it stop a Shield or Shield From Identity funder landing their own bundle twice through a new transition, as a client retrying with a fresh nonce does. Neither is closed: that would need the bundles' dummy nullifiers recorded and checked, which the platform does not do.

The same compute_platform_sighash function is used on both sides: the client uses it when signing the bundle, and the platform uses it when verifying.

Trial Decryption and Address Privacy

A shielded recipient does not appear anywhere in cleartext on-chain. To discover incoming payments, a wallet must attempt trial decryption of every new note using its Incoming Viewing Key (IVK).

The process works as follows:

  1. The wallet retrieves new note entries from the shielded pool (each entry contains a nullifier, cmx, and 216-byte encrypted_note).
  2. For each entry, the wallet constructs a CompactAction from the nullifier, commitment, ephemeral public key, and encrypted ciphertext.
  3. The wallet attempts trial decryption using try_note_decryption with its IVK.
  4. If decryption succeeds, the note was addressed to one of the wallet's OrchardAddress instances.

The nullifier stored alongside each note is essential -- it provides the Rho value needed for decryption. Without it, the Orchard protocol's forward secrecy mechanism would prevent the recipient from recovering the note.

Because a single spending key can generate unlimited OrchardAddress instances (via different diversifiers), the IVK-based scan catches all of them in a single pass. The diversifier that was used becomes apparent only after successful decryption.

Note Encryption Structure

Each SerializedAction contains an encrypted_note field of 216 bytes:

ComponentSizePurpose
epk32 bytesEphemeral public key for Diffie-Hellman key agreement
enc_ciphertext104 bytesNote plaintext encrypted to recipient (ChaCha20-Poly1305)
out_ciphertext80 bytesNote encrypted to sender for wallet recovery

The enc_ciphertext contains the note plaintext (52 bytes), a Dash-specific memo (36 bytes), and the AEAD authentication tag (16 bytes). The memo is smaller than Zcash's 512-byte memos -- Dash uses the DashMemo type (36 bytes) rather than ZcashMemo (512 bytes), keeping encrypted notes compact.

Storage in GroveDB

Notes are stored in a BulkAppendTree within the shielded credit pool:

AddressBalances / "s" (shielded_credit_pool) /
    [1] notes       -- CommitmentTree (BulkAppendTree)
    [2] nullifiers   -- Tree (spent note markers)
    [5] total_balance -- SumItem
    [6] anchors      -- Tree (block_height -> anchor)

Each note entry stores:

cmx (32 bytes) || nullifier (32 bytes) || encrypted_note (216 bytes) = 280 bytes

The nullifier is stored alongside the note (rather than separately) specifically to support trial decryption. A scanning client needs both the encrypted ciphertext and the nullifier to attempt decryption.

Rules and Guidelines

Do:

  • Use to_bech32m_string() for user-facing address display. Always include the network parameter.
  • Use to_bytes() (bincode format) for storage keys and wire serialization.
  • Generate a fresh diversifier for each new Orchard payment address to maximize unlinkability.
  • Always bind transparent fields into the platform sighash. Forgetting to include the output address in an unshield transition would be a critical vulnerability.

Do not:

  • Mix up Bech32m type bytes (0xb0, 0x80, 0x10) with bincode variant bytes (0x00, 0x01). They serve different purposes and are not interchangeable.
  • Assume Orchard addresses are interoperable with Zcash. The encoding differs (no F4Jumble, no Unified Address wrapper).
  • Use non-standard scripts in P2SH addresses. Only bare multisig (OP_M ... OP_N OP_CHECKMULTISIG) is supported.
  • Store or transmit the raw spending key. Use the Incoming Viewing Key for scanning and the Full Viewing Key for read-only wallet recovery.
  • Attempt trial decryption without the nullifier. The Orchard protocol requires the Rho derived from the nullifier to reconstruct the note.