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}