dpp/shielded/mod.rs
1#[cfg(feature = "shielded-client")]
2pub mod builder;
3
4use bincode::{Decode, Encode};
5#[cfg(feature = "serde-conversion")]
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8
9use crate::fee::Credits;
10use platform_version::version::PlatformVersion;
11
12/// Permanent storage bytes per shielded action:
13/// 280 bytes in BulkAppendTree (32 cmx + 32 rho + 216 encrypted note)
14/// + 32 bytes in nullifier tree = 312 bytes total.
15pub const SHIELDED_STORAGE_BYTES_PER_ACTION: u64 = 312;
16
17/// Domain separator for Platform sighash computation.
18const SIGHASH_DOMAIN: &[u8] = b"DashPlatformSighash";
19
20/// Computes the platform sighash from an Orchard bundle commitment and optional
21/// transparent field data.
22///
23/// The sighash is computed as:
24/// `SHA-256(SIGHASH_DOMAIN || bundle_commitment || extra_data)`
25///
26/// This binds transparent state transition fields (like `output_address` in unshield
27/// or `output_script` in shielded withdrawal) to the Orchard signatures, preventing
28/// replay attacks where an attacker substitutes transparent fields while reusing a
29/// valid Orchard bundle.
30///
31/// The same computation must be used on both the signing (client) and verification
32/// (platform) sides. For transitions without transparent fields (shield and
33/// shielded_transfer), `extra_data` is empty.
34pub fn compute_platform_sighash(bundle_commitment: &[u8; 32], extra_data: &[u8]) -> [u8; 32] {
35 let mut hasher = Sha256::new();
36 hasher.update(SIGHASH_DOMAIN);
37 hasher.update(bundle_commitment);
38 hasher.update(extra_data);
39 hasher.finalize().into()
40}
41
42/// Computes the minimum fee (in credits) for a shielded state transition.
43///
44/// The fee formula mirrors the on-chain validation in `validate_minimum_shielded_fee`:
45/// `min_fee = proof_verification_fee + num_actions × (processing_fee + storage_fee)`
46///
47/// where `storage_fee = SHIELDED_STORAGE_BYTES_PER_ACTION × (disk + processing) credits/byte`.
48///
49/// # Parameters
50/// - `num_actions` — number of Orchard actions in the bundle
51/// - `platform_version` — protocol version (determines fee constants)
52pub fn compute_minimum_shielded_fee(
53 num_actions: usize,
54 platform_version: &PlatformVersion,
55) -> Credits {
56 let constants = &platform_version
57 .drive_abci
58 .validation_and_processing
59 .event_constants;
60 let storage = &platform_version.fee_version.storage;
61 let storage_fee = SHIELDED_STORAGE_BYTES_PER_ACTION
62 * (storage.storage_disk_usage_credit_per_byte + storage.storage_processing_credit_per_byte);
63 let per_action = constants.shielded_per_action_processing_fee + storage_fee;
64 constants.shielded_proof_verification_fee + num_actions as u64 * per_action
65}
66
67/// Common Orchard bundle parameters shared across all shielded transition types.
68///
69/// Groups the fields that every shielded transition carries identically:
70/// the serialized actions, Sinsemilla anchor, Halo 2 proof, and RedPallas
71/// binding signature. Using this struct reduces parameter counts in SDK
72/// helper functions from 10-12 down to 5-8.
73pub struct OrchardBundleParams {
74 /// The serialized Orchard actions (spends + outputs).
75 pub actions: Vec<SerializedAction>,
76 /// Sinsemilla root of the note commitment tree at bundle creation time (32 bytes).
77 /// This is the Orchard Anchor — the root of the depth-32 Sinsemilla Merkle
78 /// tree over extracted note commitments (cmx values), NOT the GroveDB
79 /// commitment tree state root.
80 pub anchor: [u8; 32],
81 /// Halo 2 zero-knowledge proof bytes.
82 pub proof: Vec<u8>,
83 /// RedPallas binding signature (64 bytes) over the bundle's value balance.
84 pub binding_signature: [u8; 64],
85}
86
87/// A serialized Orchard action extracted from a bundle.
88///
89/// Each Orchard action structurally contains one spend and one output. The spend
90/// consumes a previously created note (revealing its nullifier), while the output
91/// creates a new note (publishing its commitment). Although paired in the same struct,
92/// observers cannot link which prior note was spent or what value the new note holds —
93/// the zero-knowledge proof ensures privacy.
94///
95/// These fields are raw bytes suitable for network serialization. During validation,
96/// they are parsed back into typed Orchard structs and verified via `BatchValidator`
97/// (Halo 2 proof + RedPallas signatures).
98///
99/// All fields except `spend_auth_sig` are covered by the Orchard bundle commitment
100/// (BLAKE2b-256 per ZIP-244), which feeds into the platform sighash. The signatures
101/// and proof are verified separately and are not part of the commitment.
102#[derive(Debug, Clone, Encode, Decode, PartialEq)]
103#[cfg_attr(
104 feature = "serde-conversion",
105 derive(Serialize, Deserialize),
106 serde(rename_all = "camelCase")
107)]
108pub struct SerializedAction {
109 /// Unique tag derived from the spent note's position and spending key.
110 /// Published on-chain to prevent double-spends: if this nullifier already
111 /// exists in the nullifier set, the transaction is rejected. The nullifier
112 /// is deterministic for a given note but unlinkable to the note's commitment,
113 /// preserving sender privacy.
114 pub nullifier: [u8; 32],
115
116 /// Randomized spend validating key (RedPallas verification key).
117 /// Derived from the spender's full viewing key with per-action randomness.
118 /// Used to verify `spend_auth_sig`, proving the spender controls the spending
119 /// key for the consumed note without revealing which key it is.
120 pub rk: [u8; 32],
121
122 /// Extracted note commitment for the newly created output note.
123 /// This is added to the commitment tree after the transition is applied,
124 /// allowing the recipient to later spend it. The commitment hides the note's
125 /// value, recipient, and randomness — only the recipient (who knows the
126 /// decryption key) can identify and spend this note.
127 pub cmx: [u8; 32],
128
129 /// Encrypted note ciphertext (216 bytes = epk 32 + enc_ciphertext 104 + out_ciphertext 80).
130 /// Contains the `TransmittedNoteCiphertext` fields packed contiguously:
131 /// - `epk`: ephemeral public key for Diffie-Hellman key agreement (32 bytes)
132 /// - `enc_ciphertext`: note plaintext encrypted to the recipient (104 bytes = 52 compact + 36 memo + 16 AEAD tag)
133 /// - `out_ciphertext`: encrypted to the sender for wallet recovery (80 bytes)
134 ///
135 /// Stored on-chain so recipients can scan and decrypt notes addressed to them.
136 /// Only the intended recipient (or sender) can decrypt; all others see random bytes.
137 pub encrypted_note: Vec<u8>,
138
139 /// Value commitment (Pedersen commitment to the note's value).
140 /// Commits to the value flowing through this action without revealing it.
141 /// The binding signature later proves that the sum of all `cv_net` commitments
142 /// across actions is consistent with the declared `value_balance`, ensuring
143 /// no credits are created or destroyed.
144 pub cv_net: [u8; 32],
145
146 /// RedPallas spend authorization signature over the platform sighash.
147 /// Proves the spender authorized this specific bundle (including all actions,
148 /// value_balance, anchor, and any bound transparent fields). Verified against
149 /// `rk` during batch validation. This prevents replay attacks — a valid
150 /// signature from one transition cannot be reused in another.
151 #[cfg_attr(
152 feature = "serde-conversion",
153 serde(with = "crate::serialization::serde_bytes_64")
154 )]
155 pub spend_auth_sig: [u8; 64],
156}