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/// `#[json_safe_fields]` auto-injects `#[serde(with = ...)]` on the byte fields:
103/// every `[u8; N]` → `serde_bytes` (const-generic), `Vec<u8>` → `serde_bytes_var`.
104/// Keeps the wire shape (Uint8Array in binary, base64 string in JSON) without
105/// per-field annotations.
106#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)]
107#[derive(Debug, Clone, Encode, Decode, PartialEq)]
108#[cfg_attr(
109 feature = "serde-conversion",
110 derive(Serialize, Deserialize),
111 serde(rename_all = "camelCase")
112)]
113pub struct SerializedAction {
114 /// Unique tag derived from the spent note's position and spending key.
115 /// Published on-chain to prevent double-spends: if this nullifier already
116 /// exists in the nullifier set, the transaction is rejected. The nullifier
117 /// is deterministic for a given note but unlinkable to the note's commitment,
118 /// preserving sender privacy.
119 pub nullifier: [u8; 32],
120
121 /// Randomized spend validating key (RedPallas verification key).
122 /// Derived from the spender's full viewing key with per-action randomness.
123 /// Used to verify `spend_auth_sig`, proving the spender controls the spending
124 /// key for the consumed note without revealing which key it is.
125 pub rk: [u8; 32],
126
127 /// Extracted note commitment for the newly created output note.
128 /// This is added to the commitment tree after the transition is applied,
129 /// allowing the recipient to later spend it. The commitment hides the note's
130 /// value, recipient, and randomness — only the recipient (who knows the
131 /// decryption key) can identify and spend this note.
132 pub cmx: [u8; 32],
133
134 /// Encrypted note ciphertext (216 bytes = epk 32 + enc_ciphertext 104 + out_ciphertext 80).
135 /// Contains the `TransmittedNoteCiphertext` fields packed contiguously:
136 /// - `epk`: ephemeral public key for Diffie-Hellman key agreement (32 bytes)
137 /// - `enc_ciphertext`: note plaintext encrypted to the recipient (104 bytes = 52 compact + 36 memo + 16 AEAD tag)
138 /// - `out_ciphertext`: encrypted to the sender for wallet recovery (80 bytes)
139 ///
140 /// Stored on-chain so recipients can scan and decrypt notes addressed to them.
141 /// Only the intended recipient (or sender) can decrypt; all others see random bytes.
142 pub encrypted_note: Vec<u8>,
143
144 /// Value commitment (Pedersen commitment to the note's value).
145 /// Commits to the value flowing through this action without revealing it.
146 /// The binding signature later proves that the sum of all `cv_net` commitments
147 /// across actions is consistent with the declared `value_balance`, ensuring
148 /// no credits are created or destroyed.
149 pub cv_net: [u8; 32],
150
151 /// RedPallas spend authorization signature over the platform sighash.
152 /// Proves the spender authorized this specific bundle (including all actions,
153 /// value_balance, anchor, and any bound transparent fields). Verified against
154 /// `rk` during batch validation. This prevents replay attacks — a valid
155 /// signature from one transition cannot be reused in another.
156 pub spend_auth_sig: [u8; 64],
157}