Skip to main content

dpp/shielded/
mod.rs

1#[cfg(feature = "shielded-client")]
2pub mod builder;
3
4mod compute_minimum_shielded_fee;
5pub mod memo;
6mod sighash;
7
8pub use memo::{ShieldedMemo, MEMO_PAYLOAD_SIZE, MEMO_SIZE};
9
10use bincode::{Decode, Encode};
11#[cfg(feature = "serde-conversion")]
12use serde::{Deserialize, Serialize};
13
14// Re-exported so the public path stays `dpp::shielded::compute_minimum_shielded_fee` (the
15// module and the function share a name but live in different namespaces).
16pub use compute_minimum_shielded_fee::{
17    compute_minimum_shielded_fee, compute_shielded_identity_create_fee,
18    compute_shielded_unshield_fee, compute_shielded_verification_fee,
19    compute_shielded_withdrawal_fee,
20};
21
22// Re-exported so the public paths stay `dpp::shielded::<name>` after moving the sighash preimage
23// builders into their own file. Both the version-dispatching wrappers and their `_v0` impls are
24// re-exported (callers use the wrappers; byte-layout tests use the `_v0` impls).
25pub use sighash::{
26    compute_platform_sighash, identity_create_from_shielded_extra_sighash_data,
27    identity_create_from_shielded_extra_sighash_data_v0, shielded_withdrawal_extra_sighash_data,
28    shielded_withdrawal_extra_sighash_data_v0, unshield_extra_sighash_data,
29    unshield_extra_sighash_data_v0,
30};
31
32/// Permanent storage bytes per shielded action: 344 bytes total.
33///
34/// - 312 bytes in the BulkAppendTree: 32 (`cmx`, the note commitment) + 32
35///   (`rho`) + 32 (`cv_net`, the value commitment, stored unencrypted for OVK
36///   recovery) + 216 (the encrypted note ciphertext).
37/// - 32 bytes in the nullifier tree.
38///
39/// The 216-byte encrypted note is Orchard's `TransmittedNoteCiphertext`, laid
40/// out as `epk(32) || enc_ciphertext(104) || out_ciphertext(80)`:
41///
42/// - `epk` (32): the note's ephemeral public key, published in the clear. The
43///   recipient combines it with their incoming viewing key (Diffie–Hellman) to
44///   derive the AEAD key.
45/// - `enc_ciphertext` (104): the note encrypted to the recipient (opened with
46///   the incoming viewing key) — ChaCha20-Poly1305 over the note plaintext. It
47///   holds the compact note (52 = version 1 + diversifier `d` 11 + value 8 +
48///   `rseed` 32), the memo (36), and the AEAD tag (16); the 52-byte compact
49///   prefix is what wallets trial-decrypt during sync to detect their own notes.
50/// - `out_ciphertext` (80): the note encrypted to the sender for wallet
51///   recovery (opened with the outgoing viewing key): out plaintext
52///   (64 = `pk_d` 32 + `esk` 32) + AEAD tag (16).
53///
54/// This is the standard Orchard layout except the memo is 36 bytes (`DashMemo`)
55/// instead of Zcash's 512 — the dashpay `orchard` fork makes the memo size a
56/// type parameter (`MemoSize`) — which is why each note is 216 bytes
57/// (`ENCRYPTED_NOTE_SIZE`) rather than Zcash Orchard's ~692.
58pub const SHIELDED_STORAGE_BYTES_PER_ACTION: u64 = 344;
59
60/// Calibrated effective storage-byte cost of the Core withdrawal document a
61/// `ShieldedWithdrawal` creates.
62///
63/// A `ShieldedWithdrawal` does not only write notes/nullifiers like the other pool-paid
64/// transitions — it ALSO inserts a Core withdrawal document into the withdrawals contract
65/// (`AddWithdrawalDocument`), which writes the document plus its withdrawals-contract index
66/// entries. That insert has a real, GroveDB-metered cost of ≈110,085,900 credits, which is
67/// ~98% storage and is FLAT regardless of the bundle's action count (the document and its
68/// indexes are the same size whether the withdrawal spends one note or sixteen).
69///
70/// `compute_minimum_shielded_fee` prices only the per-action note/nullifier storage and the
71/// per-bundle ZK compute, so it does NOT cover this document insert. We therefore add the
72/// document cost to the ShieldedWithdrawal fee as a flat BYTE-BASED component, sized at
73/// `SHIELDED_WITHDRAWAL_DOCUMENT_STORAGE_BYTES` effective bytes priced at the SAME per-byte
74/// storage rate the per-action note storage uses (`disk + processing` credits/byte). The
75/// measured ≈110M cost corresponds to ≈4017 effective bytes at that rate; 4100 covers it with
76/// a small (~2%) margin, and — because it is priced off the same rate — it tracks the storage
77/// rate as it evolves, exactly like the per-action note storage does. See
78/// [`compute_minimum_shielded_fee::compute_shielded_withdrawal_fee`].
79pub const SHIELDED_WITHDRAWAL_DOCUMENT_STORAGE_BYTES: u64 = 4100;
80
81/// Calibrated effective storage-byte cost of the single `AddBalanceToAddress` write an `Unshield`
82/// performs, crediting the net (`unshielding_amount − fee`) to the output platform address.
83///
84/// Like the other pool-paid transitions, an `Unshield` writes its change notes and nullifiers — but
85/// it ALSO credits a transparent platform address with `AddBalanceToAddress`. In the new-address
86/// worst case that write touches the address subtree (the address path plus its balance/nonce
87/// entries), a real, GroveDB-metered cost of ≈6,239,100 credits (≈222 of those bytes are storage)
88/// that is FLAT regardless of the bundle's action count (the address write is the same size whether
89/// the unshield spends one note or sixteen).
90///
91/// `compute_minimum_shielded_fee` prices only the per-action note/nullifier storage and the
92/// per-bundle ZK compute, so it does NOT cover this address write. We therefore add the address
93/// cost to the Unshield fee as a flat BYTE-BASED component, sized at
94/// `SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES` effective bytes priced at the SAME per-byte storage
95/// rate the per-action note storage uses (`disk + processing` credits/byte).
96///
97/// The constant is the **storage** portion of the address write: the metered `AddBalanceToAddress`
98/// op costs ≈6,239,100 credits total, of which the *storage* part is ≈6,075,000 ≈ **222 effective
99/// bytes** at the storage rate. We size the component to that storage figure — because it is a
100/// `bytes × per_byte_rate` term it is booked as storage, so it should match the address write's
101/// storage cost, not its total. The small remaining op-processing (~164K) is already covered by the
102/// per-action processing fee. Pricing it off the same rate means it tracks the storage rate as it
103/// evolves, exactly like the per-action note storage does. See
104/// [`compute_minimum_shielded_fee::compute_shielded_unshield_fee`].
105pub const SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES: u64 = 222;
106
107/// Common Orchard bundle parameters shared across all shielded transition types.
108///
109/// Groups the fields that every shielded transition carries identically:
110/// the serialized actions, Sinsemilla anchor, Halo 2 proof, and RedPallas
111/// binding signature. Using this struct reduces parameter counts in SDK
112/// helper functions from 10-12 down to 5-8.
113pub struct OrchardBundleParams {
114    /// The serialized Orchard actions (spends + outputs).
115    pub actions: Vec<SerializedAction>,
116    /// Sinsemilla root of the note commitment tree at bundle creation time (32 bytes).
117    /// This is the Orchard Anchor — the root of the depth-32 Sinsemilla Merkle
118    /// tree over extracted note commitments (cmx values), NOT the GroveDB
119    /// commitment tree state root.
120    pub anchor: [u8; 32],
121    /// Halo 2 zero-knowledge proof bytes.
122    pub proof: Vec<u8>,
123    /// RedPallas binding signature (64 bytes) over the bundle's value balance.
124    pub binding_signature: [u8; 64],
125}
126
127/// A serialized Orchard action extracted from a bundle.
128///
129/// Each Orchard action structurally contains one spend and one output. The spend
130/// consumes a previously created note (revealing its nullifier), while the output
131/// creates a new note (publishing its commitment). Although paired in the same struct,
132/// observers cannot link which prior note was spent or what value the new note holds —
133/// the zero-knowledge proof ensures privacy.
134///
135/// These fields are raw bytes suitable for network serialization. During validation,
136/// they are parsed back into typed Orchard structs and verified via `BatchValidator`
137/// (Halo 2 proof + RedPallas signatures).
138///
139/// All fields except `spend_auth_sig` are covered by the Orchard bundle commitment
140/// (BLAKE2b-256 per ZIP-244), which feeds into the platform sighash. The signatures
141/// and proof are verified separately and are not part of the commitment.
142/// `#[json_safe_fields]` auto-injects `#[serde(with = ...)]` on the byte fields:
143/// every `[u8; N]` → `serde_bytes` (const-generic), `Vec<u8>` → `serde_bytes_var`.
144/// Keeps the wire shape (Uint8Array in binary, base64 string in JSON) without
145/// per-field annotations.
146#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)]
147#[derive(Debug, Clone, Encode, Decode, PartialEq)]
148#[cfg_attr(
149    feature = "serde-conversion",
150    derive(Serialize, Deserialize),
151    serde(rename_all = "camelCase")
152)]
153pub struct SerializedAction {
154    /// Unique tag derived from the spent note's position and spending key.
155    /// Published on-chain to prevent double-spends: if this nullifier already
156    /// exists in the nullifier set, the transaction is rejected. The nullifier
157    /// is deterministic for a given note but unlinkable to the note's commitment,
158    /// preserving sender privacy.
159    pub nullifier: [u8; 32],
160
161    /// Randomized spend validating key (RedPallas verification key).
162    /// Derived from the spender's full viewing key with per-action randomness.
163    /// Used to verify `spend_auth_sig`, proving the spender controls the spending
164    /// key for the consumed note without revealing which key it is.
165    pub rk: [u8; 32],
166
167    /// Extracted note commitment for the newly created output note.
168    /// This is added to the commitment tree after the transition is applied,
169    /// allowing the recipient to later spend it. The commitment hides the note's
170    /// value, recipient, and randomness — only the recipient (who knows the
171    /// decryption key) can identify and spend this note.
172    pub cmx: [u8; 32],
173
174    /// Encrypted note ciphertext (216 bytes = epk 32 + enc_ciphertext 104 + out_ciphertext 80).
175    /// Contains the `TransmittedNoteCiphertext` fields packed contiguously:
176    /// - `epk`: ephemeral public key for Diffie-Hellman key agreement (32 bytes)
177    /// - `enc_ciphertext`: note plaintext encrypted to the recipient (104 bytes = 52 compact + 36 memo + 16 AEAD tag)
178    /// - `out_ciphertext`: encrypted to the sender for wallet recovery (80 bytes)
179    ///
180    /// Stored on-chain so recipients can scan and decrypt notes addressed to them.
181    /// Only the intended recipient (or sender) can decrypt; all others see random bytes.
182    pub encrypted_note: Vec<u8>,
183
184    /// Value commitment (Pedersen commitment to the note's value).
185    /// Commits to the value flowing through this action without revealing it.
186    /// The binding signature later proves that the sum of all `cv_net` commitments
187    /// across actions is consistent with the declared `value_balance`, ensuring
188    /// no credits are created or destroyed.
189    pub cv_net: [u8; 32],
190
191    /// RedPallas spend authorization signature over the platform sighash.
192    /// Proves the spender authorized this specific bundle (including all actions,
193    /// value_balance, anchor, and any bound transparent fields). Verified against
194    /// `rk` during batch validation. This prevents replay attacks — a valid
195    /// signature from one transition cannot be reused in another.
196    pub spend_auth_sig: [u8; 64],
197}