Skip to main content

dpp/shielded/
sighash.rs

1//! Platform sighash preimage construction for shielded transitions.
2//!
3//! Shielded transitions carry NO platform identity signature — authorization is the Orchard proof +
4//! per-action spend-auth signatures + the RedPallas binding signature over the platform sighash.
5//! These helpers build the transparent `extra_data` each transition binds into that sighash so the
6//! signing (client/builder) and verifying (consensus) sides commit to identical bytes. The byte
7//! layouts are consensus-critical and versioned via `dpp.methods.shielded_extra_sighash_data`; the
8//! credit pool's outputs-only bundles via `dpp.methods.credit_pool_bundle_binding`.
9
10use crate::address_funds::PlatformAddress;
11use crate::fee::Credits;
12use crate::identity::identity_public_key::contract_bounds::ContractBounds;
13use crate::identity::state_transition::asset_lock_proof::AssetLockProof;
14use crate::prelude::AddressNonce;
15use crate::shielded::{serialized_actions_digest, SerializedAction};
16use crate::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType;
17use crate::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Getters;
18use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation;
19use crate::withdrawal::Pooling;
20use crate::ProtocolError;
21use platform_version::version::PlatformVersion;
22use sha2::{Digest, Sha256};
23use std::collections::BTreeMap;
24
25/// Domain separator for Platform sighash computation.
26const SIGHASH_DOMAIN: &[u8] = b"DashPlatformSighash";
27
28/// The state transition type byte the token bundle of a `TokenShieldedTransferWithShieldedFee`
29/// commits to (`StateTransitionType::TokenShieldedTransferWithShieldedFee`).
30pub const TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE: u8 = 26;
31/// The state transition type byte the token bundle of a `TokenUnshieldWithShieldedFee` commits to.
32pub const TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE: u8 = 27;
33/// The state transition type byte the token bundle of a `TokenPurchaseFromShieldedPool` commits to.
34pub const TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE: u8 = 28;
35
36/// Domain tag an outputs-only token pool bundle commits to. Unlike the three constants above
37/// these are not `StateTransitionType` bytes — every one of these bundles rides inside a batch
38/// transition — yet they share a preimage slot with them at the same length. They are drawn
39/// from a high range that space has not reached, which nothing in the type system enforces:
40/// `StateTransitionType` is `repr(u8)` and could be given one of these bytes. What holds the
41/// reservation is `outputs_only_token_pool_tags_cannot_collide_with_state_transition_types`,
42/// which asks the enum and fails the build's tests the day one is assigned here.
43pub const TOKEN_SHIELD_BUNDLE_TAG: u8 = 0x80;
44/// Domain tag of a `TokenMintToPool` bundle. See [`TOKEN_SHIELD_BUNDLE_TAG`].
45pub const TOKEN_MINT_TO_POOL_BUNDLE_TAG: u8 = 0x81;
46/// Domain tag of a `TokenClaimToPool` bundle. See [`TOKEN_SHIELD_BUNDLE_TAG`].
47pub const TOKEN_CLAIM_TO_POOL_BUNDLE_TAG: u8 = 0x82;
48/// Domain tag of a `TokenDirectPurchaseToPool` bundle. See [`TOKEN_SHIELD_BUNDLE_TAG`].
49pub const TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG: u8 = 0x83;
50
51/// Domain tag of a credit pool `Shield` bundle. The credit pool's outputs-only tags continue the
52/// token pool range above: they share the same preimage slot at the same length, so every tag
53/// in both sets must stay distinct from each other and from every `StateTransitionType` byte.
54/// `credit_pool_outputs_only_tags_cannot_collide_with_state_transition_types` and
55/// `outputs_only_bundle_tags_are_pairwise_distinct` hold both reservations.
56pub const SHIELD_BUNDLE_TAG: u8 = 0x84;
57/// Domain tag of a `ShieldFromIdentity` bundle. See [`SHIELD_BUNDLE_TAG`].
58pub const SHIELD_FROM_IDENTITY_BUNDLE_TAG: u8 = 0x85;
59/// Domain tag of a `ShieldFromAssetLock` bundle. See [`SHIELD_BUNDLE_TAG`].
60pub const SHIELD_FROM_ASSET_LOCK_BUNDLE_TAG: u8 = 0x86;
61
62/// Computes the platform sighash from an Orchard bundle commitment and optional
63/// transparent field data.
64///
65/// The sighash is computed as:
66///   `SHA-256(SIGHASH_DOMAIN || bundle_commitment || extra_data)`
67///
68/// This binds transparent state transition fields (like `output_address` in unshield
69/// or `output_script` in shielded withdrawal) to the Orchard signatures, preventing
70/// replay attacks where an attacker substitutes transparent fields while reusing a
71/// valid Orchard bundle.
72///
73/// It also binds a bundle that has no transparent fields to the one context it was proved
74/// for, which an outputs-only bundle cannot do on its own: having no spends, its anchor is
75/// never checked against a pool — the client builds it against the empty tree — so it verifies
76/// against every pool.
77///
78/// The same computation must be used on both the signing (client) and verification (platform)
79/// sides. `extra_data` is empty only for the credit pool's `ShieldedTransfer`, and for the credit
80/// pool's outputs-only bundles at protocol versions that predate their binding (see
81/// [`shield_extra_sighash_data`]); each other transition has a builder in this module that
82/// spells out its layout. `ShieldedTransfer` is the one that needs no layout of its own: it
83/// spends, so it carries an anchor and nullifiers that pin it to one pool and one set of notes.
84pub fn compute_platform_sighash(bundle_commitment: &[u8; 32], extra_data: &[u8]) -> [u8; 32] {
85    let mut hasher = Sha256::new();
86    hasher.update(SIGHASH_DOMAIN);
87    hasher.update(bundle_commitment);
88    hasher.update(extra_data);
89    hasher.finalize().into()
90}
91
92/// Builds the transparent `extra_data` bound into a ShieldedWithdrawal's platform
93/// sighash, with the byte layout
94/// `output_script || unshielding_amount (u64 LE) || core_fee_per_byte (u32 LE) || pooling (u8)`.
95///
96/// Every field here is written verbatim by the transformer into the queued withdrawal
97/// document that constructs the Core asset-unlock TxOut. Binding all of them into the
98/// Orchard sighash means the binding signature authorizes them: since ShieldedWithdrawal
99/// has no identity-key signature and no address-witness check, the Orchard signature is
100/// the only authorization boundary, so a relay or block proposer cannot malleate
101/// `core_fee_per_byte` (or `pooling`, were it ever unpinned from `Never`) — e.g. flip a
102/// user's `core_fee_per_byte = 1` to a much larger Fibonacci value to redirect the
103/// withdrawn amount into L1 miner fees — without invalidating the proof.
104///
105/// The signing (client/builder) and verifying (consensus) sides MUST produce identical
106/// bytes, so both call this single function.
107///
108/// The layout places the variable-length `output_script` first with no length prefix. This
109/// is unambiguous only because `validate_structure` runs before proof verification and pins
110/// `output_script` to a canonical, fixed-length P2PKH (25 bytes) or P2SH (23 bytes); the
111/// remaining fields are fixed-width, so the preimage is well-defined for every accepted
112/// transition. If that script-shape restriction is ever relaxed, add a length prefix here.
113/// Dispatches on the platform-versioned `dpp.methods.shielded_extra_sighash_data` so the
114/// consensus-critical byte layout can evolve across protocol versions without breaking older
115/// transitions — the same versioning the sibling shielded fee methods use. The signing
116/// (client/builder) and verifying (consensus) sides both call this single function with the same
117/// `platform_version`, so they can never produce divergent preimages.
118pub fn shielded_withdrawal_extra_sighash_data(
119    output_script: &[u8],
120    unshielding_amount: u64,
121    core_fee_per_byte: u32,
122    pooling: Pooling,
123    platform_version: &PlatformVersion,
124) -> Result<Vec<u8>, ProtocolError> {
125    match platform_version.dpp.methods.shielded_extra_sighash_data {
126        0 => Ok(shielded_withdrawal_extra_sighash_data_v0(
127            output_script,
128            unshielding_amount,
129            core_fee_per_byte,
130            pooling,
131        )),
132        version => Err(ProtocolError::UnknownVersionMismatch {
133            method: "shielded_withdrawal_extra_sighash_data".to_string(),
134            known_versions: vec![0],
135            received: version,
136        }),
137    }
138}
139
140/// v0 byte layout of [`shielded_withdrawal_extra_sighash_data`] (see that function's doc comment for
141/// the layout and rationale). Frozen: never mutate; a layout change requires a new `_v1` + version.
142pub fn shielded_withdrawal_extra_sighash_data_v0(
143    output_script: &[u8],
144    unshielding_amount: u64,
145    core_fee_per_byte: u32,
146    pooling: Pooling,
147) -> Vec<u8> {
148    let mut data = Vec::with_capacity(output_script.len() + 8 + 4 + 1);
149    data.extend_from_slice(output_script);
150    data.extend_from_slice(&unshielding_amount.to_le_bytes());
151    data.extend_from_slice(&core_fee_per_byte.to_le_bytes());
152    data.push(pooling as u8);
153    data
154}
155
156/// Builds the transparent `extra_data` bound into an Unshield's platform sighash, with the
157/// byte layout `output_address || unshielding_amount (u64 LE)`.
158///
159/// As with [`shielded_withdrawal_extra_sighash_data`], the signing (client/builder) and
160/// verifying (consensus) sides MUST produce identical bytes, so both call this single
161/// function. Unshield credits a transparent platform address (not a Core asset-unlock
162/// `TxOut`), so it carries no `core_fee_per_byte`/`pooling` to bind.
163pub fn unshield_extra_sighash_data(
164    output_address: &[u8],
165    unshielding_amount: u64,
166    platform_version: &PlatformVersion,
167) -> Result<Vec<u8>, ProtocolError> {
168    match platform_version.dpp.methods.shielded_extra_sighash_data {
169        0 => Ok(unshield_extra_sighash_data_v0(
170            output_address,
171            unshielding_amount,
172        )),
173        version => Err(ProtocolError::UnknownVersionMismatch {
174            method: "unshield_extra_sighash_data".to_string(),
175            known_versions: vec![0],
176            received: version,
177        }),
178    }
179}
180
181/// v0 byte layout of [`unshield_extra_sighash_data`] (see that function's doc comment for the layout
182/// and rationale). Frozen: never mutate; a layout change requires a new `_v1` + version bump.
183pub fn unshield_extra_sighash_data_v0(output_address: &[u8], unshielding_amount: u64) -> Vec<u8> {
184    let mut data = Vec::with_capacity(output_address.len() + 8);
185    data.extend_from_slice(output_address);
186    data.extend_from_slice(&unshielding_amount.to_le_bytes());
187    data
188}
189
190/// Builds the transparent `extra_data` bound into an `IdentityTopUpFromShieldedPool`'s platform
191/// sighash, with the byte layout `identity_id (32) || top_up_amount (u64 LE)`.
192///
193/// Like `Unshield`, the transition carries no platform signature, so the state-determining
194/// transparent fields (which identity is credited, and the gross amount leaving the pool) must be
195/// committed into the Orchard binding sighash; otherwise a relayer could take a valid spend bundle
196/// and re-point it at a different identity. The client builder and the consensus verifier both
197/// call this single function.
198pub fn identity_top_up_from_shielded_extra_sighash_data(
199    identity_id: &[u8; 32],
200    top_up_amount: u64,
201    platform_version: &PlatformVersion,
202) -> Result<Vec<u8>, ProtocolError> {
203    match platform_version.dpp.methods.shielded_extra_sighash_data {
204        0 => Ok(identity_top_up_from_shielded_extra_sighash_data_v0(
205            identity_id,
206            top_up_amount,
207        )),
208        version => Err(ProtocolError::UnknownVersionMismatch {
209            method: "identity_top_up_from_shielded_extra_sighash_data".to_string(),
210            known_versions: vec![0],
211            received: version,
212        }),
213    }
214}
215
216/// v0 byte layout of [`identity_top_up_from_shielded_extra_sighash_data`]. Frozen: never mutate;
217/// a layout change requires a new `_v1` + version bump.
218pub fn identity_top_up_from_shielded_extra_sighash_data_v0(
219    identity_id: &[u8; 32],
220    top_up_amount: u64,
221) -> Vec<u8> {
222    let mut data = Vec::with_capacity(32 + 8);
223    data.extend_from_slice(identity_id);
224    data.extend_from_slice(&top_up_amount.to_le_bytes());
225    data
226}
227
228/// Builds the transparent `extra_data` bound into an `IdentityCreateFromShieldedPool`'s platform
229/// sighash, with the byte layout
230/// `identity_id (32) || denomination (u64 LE)
231///   || send_to_address_on_creation_failure (tag u8: 0=P2pkh, 1=P2sh || hash 20)
232///   || num_keys (u16 LE)
233///   || for each key in supplied order: key_id (u32 LE) || purpose (u8) || security_level (u8)
234///   || key_type (u8) || key_data_len (u16 LE) || key_data || read_only (u8)
235///   || contract_bounds (tag u8: 0=None, 1=SingleContract id(32), 2=SingleContractDocumentType
236///   id(32) name_len(u16 LE) name, 3=ContractGroup id(32))`.
237///
238/// Tag 3 is never reached: `IdentityCreateFromShieldedPool` refuses a key bound to a contract
239/// group before this preimage is built (consensus in `validate_shielded_proof` v1, the builder
240/// up front). The arm only keeps the encoder total without a panic on a block-execution path,
241/// so the v0 bytes of every reachable input are unchanged.
242///
243/// The budget and the expiry of a version 1 key are not in the layout either, and for the same
244/// reason never need to be: a key that carries either is refused at the same two places, so
245/// every key that reaches this preimage is fully described by the fields above. A version 1 key
246/// without limits binds the same bytes as its version 0 equivalent.
247///
248/// `IdentityCreateFromShieldedPool` carries NO platform identity signature: authorization is 100%
249/// the Orchard proof + per-action spend-auth signatures + binding signature over this sighash. The
250/// transparent, state-determining fields — the new identity id, the exit denomination, and the
251/// FULL public-key set — must therefore be committed into the Orchard sighash, exactly as the
252/// `surplus_output` field is committed into `ShieldFromAssetLock`'s ECDSA signature. Without this
253/// binding a relay or block proposer could take a valid bundle exiting a denomination and re-point
254/// it at a DIFFERENT identity id, or swap in DIFFERENT keys they control, stealing the credited
255/// balance (the per-key proofs-of-possession alone do NOT prevent this — a relayer keeps valid PoP
256/// sigs for their own keys while swapping the bundle). Binding `(this spend → these exact keys →
257/// this id → this denomination)` here makes the redirection atomic-or-invalid.
258///
259/// The signing (client/builder) and verifying (consensus) sides MUST produce identical bytes, so
260/// both call this single function. Unlike the fixed-length withdrawal/unshield helpers, the
261/// variable-length key list is fully length-prefixed (both the key count and each key's data) so
262/// the preimage is unambiguous for any key set.
263pub fn identity_create_from_shielded_extra_sighash_data(
264    identity_id: &[u8; 32],
265    denomination: u64,
266    send_to_address_on_creation_failure: &PlatformAddress,
267    public_keys: &[IdentityPublicKeyInCreation],
268    platform_version: &PlatformVersion,
269) -> Result<Vec<u8>, ProtocolError> {
270    match platform_version.dpp.methods.shielded_extra_sighash_data {
271        0 => Ok(identity_create_from_shielded_extra_sighash_data_v0(
272            identity_id,
273            denomination,
274            send_to_address_on_creation_failure,
275            public_keys,
276        )),
277        version => Err(ProtocolError::UnknownVersionMismatch {
278            method: "identity_create_from_shielded_extra_sighash_data".to_string(),
279            known_versions: vec![0],
280            received: version,
281        }),
282    }
283}
284
285/// v0 byte layout of [`identity_create_from_shielded_extra_sighash_data`] (see that function's doc
286/// comment for the layout and rationale). Frozen: never mutate; a layout change requires a new `_v1`
287/// + version bump.
288pub fn identity_create_from_shielded_extra_sighash_data_v0(
289    identity_id: &[u8; 32],
290    denomination: u64,
291    send_to_address_on_creation_failure: &PlatformAddress,
292    public_keys: &[IdentityPublicKeyInCreation],
293) -> Vec<u8> {
294    let mut data = Vec::with_capacity(32 + 8 + 21 + 2 + public_keys.len() * 44);
295    data.extend_from_slice(identity_id);
296    data.extend_from_slice(&denomination.to_le_bytes());
297    // Bind the fallback address (type tag || 20-byte hash) so a relayer cannot redirect the
298    // failure credit. Mirrors the way `unshield`/`withdrawal` bind their output address.
299    match send_to_address_on_creation_failure {
300        PlatformAddress::P2pkh(hash) => {
301            data.push(0u8);
302            data.extend_from_slice(hash);
303        }
304        PlatformAddress::P2sh(hash) => {
305            data.push(1u8);
306            data.extend_from_slice(hash);
307        }
308    }
309    data.extend_from_slice(&(public_keys.len() as u16).to_le_bytes());
310    for key in public_keys {
311        data.extend_from_slice(&key.id().to_le_bytes());
312        data.push(key.purpose() as u8);
313        data.push(key.security_level() as u8);
314        data.push(key.key_type() as u8);
315        let key_data = key.data().as_slice();
316        data.extend_from_slice(&(key_data.len() as u16).to_le_bytes());
317        data.extend_from_slice(key_data);
318        // Also bind `read_only` and `contract_bounds`. These are state-determining key fields that
319        // ARE in the transition's signable_bytes, but the per-key proof-of-possession does NOT bind
320        // them for hash-based key types (which accept an empty signature). Committing them into the
321        // Orchard binding sighash makes them un-malleable for EVERY key type, so a relayer/proposer
322        // cannot flip `read_only` or alter `contract_bounds` on an observed transition.
323        data.push(key.read_only() as u8);
324        match key.contract_bounds() {
325            None => data.push(0u8),
326            Some(ContractBounds::SingleContract { id }) => {
327                data.push(1u8);
328                data.extend_from_slice(id.as_bytes());
329            }
330            Some(ContractBounds::SingleContractDocumentType {
331                id,
332                document_type_name,
333            }) => {
334                data.push(2u8);
335                data.extend_from_slice(id.as_bytes());
336                let name = document_type_name.as_bytes();
337                data.extend_from_slice(&(name.len() as u16).to_le_bytes());
338                data.extend_from_slice(name);
339            }
340            Some(ContractBounds::ContractGroup { id }) => {
341                // Unreachable: refused before the preimage is built (see the layout doc).
342                data.push(3u8);
343                data.extend_from_slice(id.as_bytes());
344            }
345        }
346    }
347    data
348}
349
350/// Builds the transparent `extra_data` bound into a `TokenUnshield`'s platform sighash, with the
351/// byte layout `token_id (32) || owner_id (32) || recipient_id (32) || amount (u64 LE)`.
352///
353/// A token unshield rides inside an identity-signed batch, but the note owner who authorizes
354/// the Orchard spend is not necessarily that identity. The spend-auth and binding signatures
355/// must therefore commit to the pool the notes leave (`token_id`), the identity paying the
356/// credits fee and submitting the batch (`owner_id`), the identity credited (`recipient_id`)
357/// and the gross amount, so a bundle observed in the mempool cannot be re-wrapped by another
358/// submitter to a different recipient or against another token's pool (every token pool
359/// starts from the same empty-tree anchor).
360pub fn token_unshield_extra_sighash_data(
361    token_id: &[u8; 32],
362    owner_id: &[u8; 32],
363    recipient_id: &[u8; 32],
364    amount: u64,
365    platform_version: &PlatformVersion,
366) -> Result<Vec<u8>, ProtocolError> {
367    match platform_version.dpp.methods.shielded_extra_sighash_data {
368        0 => Ok(token_unshield_extra_sighash_data_v0(
369            token_id,
370            owner_id,
371            recipient_id,
372            amount,
373        )),
374        version => Err(ProtocolError::UnknownVersionMismatch {
375            method: "token_unshield_extra_sighash_data".to_string(),
376            known_versions: vec![0],
377            received: version,
378        }),
379    }
380}
381
382/// v0 byte layout of [`token_unshield_extra_sighash_data`]. Frozen: never mutate; a layout
383/// change requires a new `_v1` + version bump.
384pub fn token_unshield_extra_sighash_data_v0(
385    token_id: &[u8; 32],
386    owner_id: &[u8; 32],
387    recipient_id: &[u8; 32],
388    amount: u64,
389) -> Vec<u8> {
390    let mut data = Vec::with_capacity(32 + 32 + 32 + 8);
391    data.extend_from_slice(token_id);
392    data.extend_from_slice(owner_id);
393    data.extend_from_slice(recipient_id);
394    data.extend_from_slice(&amount.to_le_bytes());
395    data
396}
397
398/// Extra sighash data of a batch `TokenBurnFromPool`: the token id, the burner and the amount
399/// destroyed, so a bundle proven for one burn cannot be replayed for another token, burner or
400/// amount (72 bytes; no other layout has that length).
401///
402/// `burner_id` is the identity the burn is attributed to: the batch owner of a direct burn, or
403/// the proposer of a group action burn. A group action pins the digest of the bundle's actions
404/// (spend authorization signatures included), so every other signer submits the proposer's
405/// bundle unchanged and the sighash must not depend on whose batch carries it.
406pub fn token_burn_from_pool_extra_sighash_data(
407    token_id: &[u8; 32],
408    burner_id: &[u8; 32],
409    amount: u64,
410    platform_version: &PlatformVersion,
411) -> Result<Vec<u8>, ProtocolError> {
412    match platform_version.dpp.methods.shielded_extra_sighash_data {
413        0 => Ok(token_burn_from_pool_extra_sighash_data_v0(
414            token_id, burner_id, amount,
415        )),
416        version => Err(ProtocolError::UnknownVersionMismatch {
417            method: "token_burn_from_pool_extra_sighash_data".to_string(),
418            known_versions: vec![0],
419            received: version,
420        }),
421    }
422}
423
424/// Version 0 layout: `token_id (32) || burner_id (32) || amount (8, little endian)`.
425pub fn token_burn_from_pool_extra_sighash_data_v0(
426    token_id: &[u8; 32],
427    burner_id: &[u8; 32],
428    amount: u64,
429) -> Vec<u8> {
430    let mut data = Vec::with_capacity(32 + 32 + 8);
431    data.extend_from_slice(token_id);
432    data.extend_from_slice(burner_id);
433    data.extend_from_slice(&amount.to_le_bytes());
434    data
435}
436
437/// Extra sighash data of a document action paid from a token shielded pool
438/// (`TokenPaymentInfo::V1`): the token id, the batch owner, the document's contract and id
439/// and the amount paid, so a bundle proven for one document cannot be replayed for another
440/// document, batch owner, token or cost.
441pub fn document_token_payment_extra_sighash_data(
442    token_id: &[u8; 32],
443    owner_id: &[u8; 32],
444    data_contract_id: &[u8; 32],
445    document_id: &[u8; 32],
446    amount: u64,
447    platform_version: &PlatformVersion,
448) -> Result<Vec<u8>, ProtocolError> {
449    match platform_version.dpp.methods.shielded_extra_sighash_data {
450        0 => Ok(document_token_payment_extra_sighash_data_v0(
451            token_id,
452            owner_id,
453            data_contract_id,
454            document_id,
455            amount,
456        )),
457        version => Err(ProtocolError::UnknownVersionMismatch {
458            method: "document_token_payment_extra_sighash_data".to_string(),
459            known_versions: vec![0],
460            received: version,
461        }),
462    }
463}
464
465/// Version 0 layout: `token_id (32) || owner_id (32) || data_contract_id (32) ||
466/// document_id (32) || amount (8, little endian)`. Frozen: never mutate; a layout change
467/// requires a new `_v1` + version bump.
468pub fn document_token_payment_extra_sighash_data_v0(
469    token_id: &[u8; 32],
470    owner_id: &[u8; 32],
471    data_contract_id: &[u8; 32],
472    document_id: &[u8; 32],
473    amount: u64,
474) -> Vec<u8> {
475    let mut data = Vec::with_capacity(32 * 4 + 8);
476    data.extend_from_slice(token_id);
477    data.extend_from_slice(owner_id);
478    data.extend_from_slice(data_contract_id);
479    data.extend_from_slice(document_id);
480    data.extend_from_slice(&amount.to_le_bytes());
481    data
482}
483
484/// Extra sighash data of the token bundle of a `TokenShieldedTransferWithShieldedFee`: the
485/// state transition type and the token id, so the bundle is pinned to one token pool and one
486/// transition kind.
487pub fn token_shielded_transfer_with_shielded_fee_extra_sighash_data(
488    token_id: &[u8; 32],
489    platform_version: &PlatformVersion,
490) -> Result<Vec<u8>, ProtocolError> {
491    match platform_version.dpp.methods.shielded_extra_sighash_data {
492        0 => Ok(token_shielded_transfer_with_shielded_fee_extra_sighash_data_v0(token_id)),
493        version => Err(ProtocolError::UnknownVersionMismatch {
494            method: "token_shielded_transfer_with_shielded_fee_extra_sighash_data".to_string(),
495            known_versions: vec![0],
496            received: version,
497        }),
498    }
499}
500
501/// Version 0 layout: `state transition type (1, = 26) || token_id (32)`. Frozen.
502pub fn token_shielded_transfer_with_shielded_fee_extra_sighash_data_v0(
503    token_id: &[u8; 32],
504) -> Vec<u8> {
505    let mut data = Vec::with_capacity(1 + 32);
506    data.push(TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE);
507    data.extend_from_slice(token_id);
508    data
509}
510
511/// Extra sighash data of the token bundle of a `TokenUnshieldWithShieldedFee`: the state
512/// transition type, the token id, the recipient and the amount, so the bundle cannot be
513/// replayed against another token, recipient or amount.
514pub fn token_unshield_with_shielded_fee_extra_sighash_data(
515    token_id: &[u8; 32],
516    recipient_id: &[u8; 32],
517    amount: u64,
518    platform_version: &PlatformVersion,
519) -> Result<Vec<u8>, ProtocolError> {
520    match platform_version.dpp.methods.shielded_extra_sighash_data {
521        0 => Ok(token_unshield_with_shielded_fee_extra_sighash_data_v0(
522            token_id,
523            recipient_id,
524            amount,
525        )),
526        version => Err(ProtocolError::UnknownVersionMismatch {
527            method: "token_unshield_with_shielded_fee_extra_sighash_data".to_string(),
528            known_versions: vec![0],
529            received: version,
530        }),
531    }
532}
533
534/// Version 0 layout: `state transition type (1, = 27) || token_id (32) || recipient_id (32)
535/// || amount (8, little endian)`. Frozen.
536pub fn token_unshield_with_shielded_fee_extra_sighash_data_v0(
537    token_id: &[u8; 32],
538    recipient_id: &[u8; 32],
539    amount: u64,
540) -> Vec<u8> {
541    let mut data = Vec::with_capacity(1 + 32 + 32 + 8);
542    data.push(TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE);
543    data.extend_from_slice(token_id);
544    data.extend_from_slice(recipient_id);
545    data.extend_from_slice(&amount.to_le_bytes());
546    data
547}
548
549/// Extra sighash data of the token bundle of a `TokenPurchaseFromShieldedPool`: the state
550/// transition type, the token id, the token count and the agreed price.
551pub fn token_purchase_from_shielded_pool_extra_sighash_data(
552    token_id: &[u8; 32],
553    token_count: u64,
554    total_agreed_price: u64,
555    platform_version: &PlatformVersion,
556) -> Result<Vec<u8>, ProtocolError> {
557    match platform_version.dpp.methods.shielded_extra_sighash_data {
558        0 => Ok(token_purchase_from_shielded_pool_extra_sighash_data_v0(
559            token_id,
560            token_count,
561            total_agreed_price,
562        )),
563        version => Err(ProtocolError::UnknownVersionMismatch {
564            method: "token_purchase_from_shielded_pool_extra_sighash_data".to_string(),
565            known_versions: vec![0],
566            received: version,
567        }),
568    }
569}
570
571/// Version 0 layout: `state transition type (1, = 28) || token_id (32) || token_count (8, LE)
572/// || total_agreed_price (8, LE)`. Frozen.
573pub fn token_purchase_from_shielded_pool_extra_sighash_data_v0(
574    token_id: &[u8; 32],
575    token_count: u64,
576    total_agreed_price: u64,
577) -> Vec<u8> {
578    let mut data = Vec::with_capacity(1 + 32 + 8 + 8);
579    data.push(TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE);
580    data.extend_from_slice(token_id);
581    data.extend_from_slice(&token_count.to_le_bytes());
582    data.extend_from_slice(&total_agreed_price.to_le_bytes());
583    data
584}
585
586/// Extra sighash data of the credit pool fee bundle of an identity-less token pool transition:
587/// the state transition type, the token id and a digest of the token bundle's actions, so the
588/// fee bundle can only ever pay for that exact token bundle.
589pub fn token_pool_fee_bundle_extra_sighash_data(
590    state_transition_type: u8,
591    token_id: &[u8; 32],
592    token_actions: &[SerializedAction],
593    platform_version: &PlatformVersion,
594) -> Result<Vec<u8>, ProtocolError> {
595    match platform_version.dpp.methods.shielded_extra_sighash_data {
596        0 => Ok(token_pool_fee_bundle_extra_sighash_data_v0(
597            state_transition_type,
598            token_id,
599            &serialized_actions_digest(token_actions),
600        )),
601        version => Err(ProtocolError::UnknownVersionMismatch {
602            method: "token_pool_fee_bundle_extra_sighash_data".to_string(),
603            known_versions: vec![0],
604            received: version,
605        }),
606    }
607}
608
609/// Version 0 layout: `state transition type (1) || token_id (32) || token actions digest (32)`.
610/// Frozen.
611pub fn token_pool_fee_bundle_extra_sighash_data_v0(
612    state_transition_type: u8,
613    token_id: &[u8; 32],
614    token_actions_digest: &[u8; 32],
615) -> Vec<u8> {
616    let mut data = Vec::with_capacity(1 + 32 + 32);
617    data.push(state_transition_type);
618    data.extend_from_slice(token_id);
619    data.extend_from_slice(token_actions_digest);
620    data
621}
622
623/// Builds the transparent `extra_data` bound into a `TokenShieldedTransfer`'s platform sighash,
624/// with the byte layout `token_id (32) || owner_id (32)`.
625///
626/// Nothing leaves the pool, so there is no amount or destination to bind, but the bundle is
627/// still pinned to one token pool and to the identity that pays for it: the same bundle
628/// cannot be resubmitted under another fee payer, and the pool it spends from is explicit.
629pub fn token_shielded_transfer_extra_sighash_data(
630    token_id: &[u8; 32],
631    owner_id: &[u8; 32],
632    platform_version: &PlatformVersion,
633) -> Result<Vec<u8>, ProtocolError> {
634    match platform_version.dpp.methods.shielded_extra_sighash_data {
635        0 => Ok(token_shielded_transfer_extra_sighash_data_v0(
636            token_id, owner_id,
637        )),
638        version => Err(ProtocolError::UnknownVersionMismatch {
639            method: "token_shielded_transfer_extra_sighash_data".to_string(),
640            known_versions: vec![0],
641            received: version,
642        }),
643    }
644}
645
646/// v0 byte layout of [`token_shielded_transfer_extra_sighash_data`]. Frozen: never mutate; a
647/// layout change requires a new `_v1` + version bump.
648pub fn token_shielded_transfer_extra_sighash_data_v0(
649    token_id: &[u8; 32],
650    owner_id: &[u8; 32],
651) -> Vec<u8> {
652    let mut data = Vec::with_capacity(64);
653    data.extend_from_slice(token_id);
654    data.extend_from_slice(owner_id);
655    data
656}
657
658/// Extra sighash data of an outputs-only token pool bundle — `TokenShield`, `TokenMintToPool`,
659/// `TokenClaimToPool` and `TokenDirectPurchaseToPool`: the bundle's domain tag, the token id
660/// and the identity the bundle is attributed to.
661///
662/// These bundles have no spends, so nothing pins them to a pool: their anchor is never checked
663/// against one — the client builds it against the empty tree — and the proof and binding
664/// signature verify against any pool. Without this data the authorized bundle bytes are a
665/// free-standing, self-verifying object that anybody can lift out of the mempool into a
666/// transition of their own. Each of the three fields closes one direction of that:
667///
668/// - **The token id** keeps a bundle out of every other token's pool. That pool has its own
669///   nullifier tree, so a copy there would leave both notes spendable; the harm is that the
670///   recipient would hold notes derived from one `rho` in two pools, which links their spends
671///   across pools.
672/// - **The tag** keeps the kinds' preimages disjoint in the same pool. Bundles of different
673///   kinds are otherwise indistinguishable to the proof — same flags, same empty-tree anchor,
674///   and the same value balance whenever the amounts match. A same-pool reuse is also caught by
675///   the nullifier record described below.
676/// - **The owner** keeps it out of every other identity's transition of the same kind into the
677///   same pool. Without it a copier could take a bundle from the mempool and land it ahead of
678///   its author, funding the notes the author built with its own tokens, credits or claim, or
679///   minting them with its own authority. The author's own transition would then be refused on
680///   its recorded dummy nullifier and charged its fee.
681///
682/// `owner_id` is the batch owner, except for a group action mint, where it is the group
683/// action's proposer: a group action pins the digest of the bundle's actions, so every other
684/// signer submits the proposer's bundle unchanged and the preimage must not depend on whose
685/// batch carries it. `TokenShield`, `TokenClaimToPool` and `TokenDirectPurchaseToPool` are
686/// never group actions.
687///
688/// The preimage does not stop the owner from submitting its own bundle twice. That, and any
689/// other repeat of a bundle into the same pool, consensus refuses on the state side: every
690/// token pool write that takes an outputs-only bundle records the bundle's dummy nullifiers in
691/// the pool's nullifier tree, and a bundle whose dummy nullifier is already there is refused
692/// with `NullifierAlreadySpentError`. A repeat that got through would land a second note with
693/// the same commitment and the same `rho`, hence the same nullifier, of which only one could
694/// ever be spent.
695pub fn token_pool_output_only_extra_sighash_data(
696    action_type: TokenTransitionActionType,
697    token_id: &[u8; 32],
698    owner_id: &[u8; 32],
699    platform_version: &PlatformVersion,
700) -> Result<Vec<u8>, ProtocolError> {
701    match platform_version.dpp.methods.shielded_extra_sighash_data {
702        0 => Ok(token_pool_output_only_extra_sighash_data_v0(
703            outputs_only_bundle_tag_v0(action_type)?,
704            token_id,
705            owner_id,
706        )),
707        version => Err(ProtocolError::UnknownVersionMismatch {
708            method: "token_pool_output_only_extra_sighash_data".to_string(),
709            known_versions: vec![0],
710            received: version,
711        }),
712    }
713}
714
715/// The v0 domain tag of an outputs-only token pool bundle. Frozen: the mapping is part of the
716/// sighash preimage, so a kind keeps its byte forever and a new kind takes a new one.
717///
718/// The bytes are written out here rather than derived from the variant's position, so the
719/// preimage does not depend on the enum's layout at all. `TokenTransitionActionType` is marked
720/// append-only, but that marker is a convention for clients; resting a consensus preimage on it
721/// would turn an accidental reordering into a silent wire change.
722///
723/// Every other kind is named rather than swept up by a catch-all: a kind added to the enum must
724/// fail to compile here, so that whoever adds it decides whether it carries a bundle. A
725/// catch-all would silently answer "no" and surface as a rejected block instead.
726fn outputs_only_bundle_tag_v0(action_type: TokenTransitionActionType) -> Result<u8, ProtocolError> {
727    match action_type {
728        TokenTransitionActionType::Shield => Ok(TOKEN_SHIELD_BUNDLE_TAG),
729        TokenTransitionActionType::MintToPool => Ok(TOKEN_MINT_TO_POOL_BUNDLE_TAG),
730        TokenTransitionActionType::ClaimToPool => Ok(TOKEN_CLAIM_TO_POOL_BUNDLE_TAG),
731        TokenTransitionActionType::DirectPurchaseToPool => {
732            Ok(TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG)
733        }
734        other @ (TokenTransitionActionType::Burn
735        | TokenTransitionActionType::Mint
736        | TokenTransitionActionType::Transfer
737        | TokenTransitionActionType::Freeze
738        | TokenTransitionActionType::Unfreeze
739        | TokenTransitionActionType::DestroyFrozenFunds
740        | TokenTransitionActionType::Claim
741        | TokenTransitionActionType::EmergencyAction
742        | TokenTransitionActionType::ConfigUpdate
743        | TokenTransitionActionType::DirectPurchase
744        | TokenTransitionActionType::SetPriceForDirectPurchase
745        | TokenTransitionActionType::BurnFromPool
746        | TokenTransitionActionType::Unshield
747        | TokenTransitionActionType::ShieldedTransfer) => {
748            Err(ProtocolError::InvalidStateTransitionType(format!(
749                "{other} is not an outputs-only token pool transition and has no bundle tag"
750            )))
751        }
752    }
753}
754
755/// Version 0 layout: `bundle tag (1) || token_id (32) || owner_id (32)`. Frozen: never
756/// mutate; a layout change requires a new `_v1` + version bump.
757pub fn token_pool_output_only_extra_sighash_data_v0(
758    bundle_tag: u8,
759    token_id: &[u8; 32],
760    owner_id: &[u8; 32],
761) -> Vec<u8> {
762    let mut data = Vec::with_capacity(1 + 32 + 32);
763    data.push(bundle_tag);
764    data.extend_from_slice(token_id);
765    data.extend_from_slice(owner_id);
766    data
767}
768
769/// Extra sighash data of a credit pool `Shield` bundle: its kind tag and a digest of the platform
770/// addresses that fund it. See [`credit_pool_output_only_extra_sighash_data_v0`] for why the
771/// credit pool's outputs-only bundles bind anything at all.
772///
773/// A `Shield` has no identity, so its owner is its funding: the SHA-256 of its input addresses,
774/// each in its 21-byte encoding ([`PlatformAddress::to_bytes`]), in the order the `inputs` map
775/// holds them. That order is part of the wire format. It is the map's key order, which is also
776/// the order the transition serializes its inputs in, so a client that assembles the same set in
777/// any other order still binds the same bytes.
778///
779/// The nonces and the contributed amounts are deliberately left out. The owner answers "who
780/// funds this", not "which transition carries it", exactly as `ShieldFromIdentity` binds its
781/// identity and not its nonce: the preimage is there to stop somebody else from re-wrapping the
782/// bundle. Binding the nonce would not stop the funder's own duplicates either, because a sender
783/// can rebuild the same notes under any preimage (see
784/// [`credit_pool_output_only_extra_sighash_data_v0`]); closing that would need the bundles' dummy
785/// nullifiers recorded and checked, which consensus does not do. Adding the nonce later would
786/// change the layout under every bundle already built for this one.
787///
788/// Dispatches on `dpp.methods.credit_pool_bundle_binding`, which the client builder and the
789/// consensus verifier both read: `None` binds nothing (the empty preimage every shipped verifier
790/// expects), `Some(0)` binds `tag (1) || funding digest (32)`.
791pub fn shield_extra_sighash_data(
792    inputs: &BTreeMap<PlatformAddress, (AddressNonce, Credits)>,
793    platform_version: &PlatformVersion,
794) -> Result<Vec<u8>, ProtocolError> {
795    match platform_version.dpp.methods.credit_pool_bundle_binding {
796        None => Ok(Vec::new()),
797        Some(0) => Ok(credit_pool_output_only_extra_sighash_data_v0(
798            SHIELD_BUNDLE_TAG,
799            &shield_funding_digest_v0(inputs),
800        )),
801        Some(version) => Err(ProtocolError::UnknownVersionMismatch {
802            method: "shield_extra_sighash_data".to_string(),
803            known_versions: vec![0],
804            received: version,
805        }),
806    }
807}
808
809/// The version 0 owner of a `Shield` bundle: SHA-256 over the 21-byte encoding of each input
810/// address, in the map's key order. Frozen: never mutate; see [`shield_extra_sighash_data`].
811fn shield_funding_digest_v0(
812    inputs: &BTreeMap<PlatformAddress, (AddressNonce, Credits)>,
813) -> [u8; 32] {
814    let mut hasher = Sha256::new();
815    for address in inputs.keys() {
816        hasher.update(address.to_bytes());
817    }
818    hasher.finalize().into()
819}
820
821/// Extra sighash data of a `ShieldFromIdentity` bundle: its kind tag and the identity whose
822/// balance funds it. See [`credit_pool_output_only_extra_sighash_data_v0`].
823///
824/// The identity signature already covers the bundle, but only this transition's: nothing in the
825/// bundle itself says which identity it was proved for, so without this preimage any other
826/// identity could sign a transition of its own around the same proved bytes. The funding identity
827/// itself can still land the same bundle again under a new nonce, which is what a client retrying
828/// with a fresh nonce does; stopping that would need the bundles' dummy nullifiers recorded and
829/// checked, which consensus does not do.
830///
831/// Dispatches on `dpp.methods.credit_pool_bundle_binding` like [`shield_extra_sighash_data`].
832pub fn shield_from_identity_extra_sighash_data(
833    identity_id: &[u8; 32],
834    platform_version: &PlatformVersion,
835) -> Result<Vec<u8>, ProtocolError> {
836    match platform_version.dpp.methods.credit_pool_bundle_binding {
837        None => Ok(Vec::new()),
838        Some(0) => Ok(credit_pool_output_only_extra_sighash_data_v0(
839            SHIELD_FROM_IDENTITY_BUNDLE_TAG,
840            identity_id,
841        )),
842        Some(version) => Err(ProtocolError::UnknownVersionMismatch {
843            method: "shield_from_identity_extra_sighash_data".to_string(),
844            known_versions: vec![0],
845            received: version,
846        }),
847    }
848}
849
850/// Extra sighash data of a `ShieldFromAssetLock` bundle: its kind tag and the identifier of the
851/// asset lock that funds it ([`AssetLockProof::create_identifier`], the double SHA-256 of the
852/// locked outpoint). See [`credit_pool_output_only_extra_sighash_data_v0`].
853///
854/// Binding the full outpoint, rather than the transaction id, makes this binding single-use. A
855/// successful shield consumes the whole lock, so a given bundle can land at most once, whoever
856/// wraps it and however often it is resubmitted: a copier would have to fund it from this very
857/// lock, and so would the sender's own retry. Binding only the transaction id would let anybody
858/// holding another credit output of the same asset-lock transaction lift the bundle. A sender who
859/// deliberately rebuilds the same notes under another lock is still not stopped; see
860/// [`credit_pool_output_only_extra_sighash_data_v0`].
861///
862/// The identifier is the same one an identity created from that lock would get; the kind tag keeps
863/// this preimage apart from a `ShieldFromIdentity` of that identity.
864///
865/// Dispatches on `dpp.methods.credit_pool_bundle_binding` like [`shield_extra_sighash_data`].
866pub fn shield_from_asset_lock_extra_sighash_data(
867    asset_lock_proof: &AssetLockProof,
868    platform_version: &PlatformVersion,
869) -> Result<Vec<u8>, ProtocolError> {
870    match platform_version.dpp.methods.credit_pool_bundle_binding {
871        None => Ok(Vec::new()),
872        Some(0) => Ok(credit_pool_output_only_extra_sighash_data_v0(
873            SHIELD_FROM_ASSET_LOCK_BUNDLE_TAG,
874            &asset_lock_proof.create_identifier()?.to_buffer(),
875        )),
876        Some(version) => Err(ProtocolError::UnknownVersionMismatch {
877            method: "shield_from_asset_lock_extra_sighash_data".to_string(),
878            known_versions: vec![0],
879            received: version,
880        }),
881    }
882}
883
884/// Version 0 layout of the credit pool's outputs-only bundles — `Shield`, `ShieldFromIdentity` and
885/// `ShieldFromAssetLock`: `bundle tag (1) || owner (32)`. Frozen: never mutate; a layout change
886/// requires a new `credit_pool_bundle_binding` version.
887///
888/// These bundles have no spends, so they carry no anchor to pin them to anything: the proof and
889/// the binding signature verify wherever they are submitted. Unbound, the authorized bundle bytes
890/// are a free-standing, self-verifying object: anybody can lift them out of the mempool into a
891/// transition of their own, funded by their own credits, and any bundle ever published can be
892/// replayed. The copier pays the full amount to the original recipient and gains nothing, but the
893/// copy lands a second note with the same commitment and the same nullifier in the credit pool,
894/// whose notes share one nullifier set, so only one of the two can ever be spent.
895///
896/// The owner is what a third party cannot authorize: the addresses, identity or asset lock whose
897/// signatures fund the original. The tag stops a bundle proved for one kind from being submitted as another,
898/// which the owner alone would not: the three kinds are otherwise indistinguishable to the proof,
899/// and an identity created from an asset lock has that lock's identifier as its id.
900///
901/// This does not reach Faerie Gold. The `rho` of an outputs-only note is the nullifier of its
902/// bundle's dummy spend, so a sender who builds and signs a fresh bundle reusing the same dummy
903/// spend note and `rseed` gets the same commitment and the same nullifier under any preimage, and
904/// a recipient counting deposits by commitment credits two where only one can be spent. Nor does
905/// it stop the funder landing their own bundle twice through a new transition, as a client that
906/// retries with a fresh nonce does — except for `ShieldFromAssetLock`, whose lock can fund one
907/// successful shield only (see [`shield_from_asset_lock_extra_sighash_data`]). Closing either
908/// would need the bundles' dummy nullifiers recorded and checked, which consensus does not do.
909pub fn credit_pool_output_only_extra_sighash_data_v0(bundle_tag: u8, owner: &[u8; 32]) -> Vec<u8> {
910    let mut data = Vec::with_capacity(1 + 32);
911    data.push(bundle_tag);
912    data.extend_from_slice(owner);
913    data
914}
915
916#[cfg(test)]
917mod tests {
918    use super::*;
919    use crate::identity::core_script::CoreScript;
920    use crate::state_transition::StateTransitionType;
921    use crate::withdrawal::Pooling;
922    // These tests pin the v0 preimage directly (they assert exact bytes), so resolve the bare helper
923    // names to the `_v0` impls rather than the version-dispatching public wrappers.
924    use crate::shielded::shielded_withdrawal_extra_sighash_data_v0 as shielded_withdrawal_extra_sighash_data;
925    use crate::shielded::unshield_extra_sighash_data_v0 as unshield_extra_sighash_data;
926
927    #[test]
928    fn withdrawal_sighash_data_binds_core_fee_per_byte() {
929        let script = CoreScript::new_p2pkh([1u8; 20]);
930        let a = shielded_withdrawal_extra_sighash_data(script.as_bytes(), 1000, 1, Pooling::Never);
931        let b = shielded_withdrawal_extra_sighash_data(script.as_bytes(), 1000, 2, Pooling::Never);
932        assert_ne!(
933            a, b,
934            "changing core_fee_per_byte must change the sighash preimage"
935        );
936    }
937
938    #[test]
939    fn withdrawal_sighash_data_binds_pooling() {
940        // `pooling` is pinned to `Never` by `validate_structure`, so this binding is currently
941        // dead defense-in-depth; assert it is nonetheless mixed into the preimage so a future
942        // unpinning would still be authorized by the Orchard binding signature.
943        let script = CoreScript::new_p2pkh([1u8; 20]);
944        let a = shielded_withdrawal_extra_sighash_data(script.as_bytes(), 1000, 1, Pooling::Never);
945        let b = shielded_withdrawal_extra_sighash_data(
946            script.as_bytes(),
947            1000,
948            1,
949            Pooling::IfAvailable,
950        );
951        assert_ne!(a, b, "changing pooling must change the sighash preimage");
952    }
953
954    #[test]
955    fn withdrawal_sighash_data_layout() {
956        // output_script(2) || unshielding_amount(8) || core_fee_per_byte(4) || pooling(1)
957        let d = shielded_withdrawal_extra_sighash_data(&[0xAA, 0xBB], 1, 2, Pooling::Never);
958        assert_eq!(d.len(), 2 + 8 + 4 + 1);
959        assert_eq!(&d[0..2], &[0xAA, 0xBB]);
960        assert_eq!(&d[2..10], &1u64.to_le_bytes());
961        assert_eq!(&d[10..14], &2u32.to_le_bytes());
962        assert_eq!(d[14], Pooling::Never as u8);
963    }
964
965    #[test]
966    fn token_unshield_sighash_data_layout() {
967        use crate::shielded::token_unshield_extra_sighash_data_v0;
968        // token_id(32) || owner_id(32) || recipient_id(32) || amount(8)
969        let d =
970            token_unshield_extra_sighash_data_v0(&[0xAAu8; 32], &[0xBBu8; 32], &[0xCCu8; 32], 7);
971        assert_eq!(d.len(), 32 + 32 + 32 + 8);
972        assert_eq!(&d[0..32], &[0xAAu8; 32]);
973        assert_eq!(&d[32..64], &[0xBBu8; 32]);
974        assert_eq!(&d[64..96], &[0xCCu8; 32]);
975        assert_eq!(&d[96..104], &7u64.to_le_bytes());
976        // Every bound field changes the preimage.
977        assert_ne!(
978            d,
979            token_unshield_extra_sighash_data_v0(&[0xADu8; 32], &[0xBBu8; 32], &[0xCCu8; 32], 7)
980        );
981        assert_ne!(
982            d,
983            token_unshield_extra_sighash_data_v0(&[0xAAu8; 32], &[0xB0u8; 32], &[0xCCu8; 32], 7)
984        );
985        assert_ne!(
986            d,
987            token_unshield_extra_sighash_data_v0(&[0xAAu8; 32], &[0xBBu8; 32], &[0xC0u8; 32], 7)
988        );
989        assert_ne!(
990            d,
991            token_unshield_extra_sighash_data_v0(&[0xAAu8; 32], &[0xBBu8; 32], &[0xCCu8; 32], 8)
992        );
993    }
994
995    #[test]
996    fn token_shielded_transfer_sighash_data_layout() {
997        use crate::shielded::token_shielded_transfer_extra_sighash_data_v0;
998        // token_id(32) || owner_id(32)
999        let d = token_shielded_transfer_extra_sighash_data_v0(&[0x11u8; 32], &[0x22u8; 32]);
1000        assert_eq!(d.len(), 64);
1001        assert_eq!(&d[0..32], &[0x11u8; 32]);
1002        assert_eq!(&d[32..64], &[0x22u8; 32]);
1003        assert_ne!(
1004            d,
1005            token_shielded_transfer_extra_sighash_data_v0(&[0x12u8; 32], &[0x22u8; 32])
1006        );
1007        assert_ne!(
1008            d,
1009            token_shielded_transfer_extra_sighash_data_v0(&[0x11u8; 32], &[0x23u8; 32])
1010        );
1011    }
1012
1013    #[test]
1014    fn unshield_sighash_data_layout() {
1015        // output_address || unshielding_amount(8)
1016        let d = unshield_extra_sighash_data(&[0xAA, 0xBB, 0xCC], 5);
1017        assert_eq!(d.len(), 3 + 8);
1018        assert_eq!(&d[0..3], &[0xAA, 0xBB, 0xCC]);
1019        assert_eq!(&d[3..11], &5u64.to_le_bytes());
1020    }
1021
1022    mod identity_create_sighash {
1023        use super::*;
1024        // Pin the v0 preimage directly (see the note in the parent test module).
1025        use crate::identity::{KeyType, Purpose, SecurityLevel};
1026        use crate::shielded::identity_create_from_shielded_extra_sighash_data_v0 as identity_create_from_shielded_extra_sighash_data;
1027        use crate::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0;
1028        use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation;
1029        use platform_value::BinaryData;
1030
1031        fn mk_key(id: u32, data_byte: u8) -> IdentityPublicKeyInCreation {
1032            IdentityPublicKeyInCreation::V0(IdentityPublicKeyInCreationV0 {
1033                id,
1034                key_type: KeyType::ECDSA_SECP256K1,
1035                purpose: Purpose::AUTHENTICATION,
1036                security_level: SecurityLevel::MASTER,
1037                contract_bounds: None,
1038                read_only: false,
1039                data: BinaryData::new(vec![data_byte; 33]),
1040                signature: BinaryData::new(vec![]),
1041            })
1042        }
1043
1044        #[test]
1045        fn layout_is_length_prefixed() {
1046            // identity_id(32) || denomination(8)
1047            //   || send_to_address_on_creation_failure (tag(1) || hash(20))
1048            //   || num_keys(2)
1049            //   || [key_id(4)|purpose|sec|type|len(2)|data|read_only(1)|contract_bounds_tag(1)]
1050            let id = [0x11u8; 32];
1051            let keys = vec![mk_key(7, 0xAB)];
1052            let fallback = PlatformAddress::P2pkh([0x5Cu8; 20]);
1053            let d = identity_create_from_shielded_extra_sighash_data(
1054                &id,
1055                10_000_000_000,
1056                &fallback,
1057                &keys,
1058            );
1059            assert_eq!(&d[0..32], &id);
1060            assert_eq!(&d[32..40], &10_000_000_000u64.to_le_bytes());
1061            // Fallback address: tag(0=P2pkh) at offset 40, 20-byte hash at 41..61.
1062            assert_eq!(d[40], 0u8, "fallback address P2pkh tag");
1063            assert_eq!(&d[41..61], &[0x5Cu8; 20], "fallback address hash");
1064            assert_eq!(&d[61..63], &1u16.to_le_bytes());
1065            assert_eq!(&d[63..67], &7u32.to_le_bytes());
1066            assert_eq!(d[67], Purpose::AUTHENTICATION as u8);
1067            assert_eq!(d[68], SecurityLevel::MASTER as u8);
1068            assert_eq!(d[69], KeyType::ECDSA_SECP256K1 as u8);
1069            assert_eq!(&d[70..72], &33u16.to_le_bytes());
1070            assert_eq!(&d[72..105], &[0xAB; 33]);
1071            assert_eq!(d[105], 0u8, "read_only=false");
1072            assert_eq!(d[106], 0u8, "contract_bounds=None tag");
1073            assert_eq!(d.len(), 32 + 8 + 21 + 2 + (4 + 1 + 1 + 1 + 2 + 33 + 1 + 1));
1074        }
1075
1076        #[test]
1077        fn binds_identity_id_denomination_and_keys() {
1078            let id_a = [0x11u8; 32];
1079            let id_b = [0x22u8; 32];
1080            let keys = vec![mk_key(0, 0xAA)];
1081            let fallback = PlatformAddress::P2pkh([0x01u8; 20]);
1082            let base = identity_create_from_shielded_extra_sighash_data(
1083                &id_a,
1084                10_000_000_000,
1085                &fallback,
1086                &keys,
1087            );
1088
1089            // Changing the identity id changes the preimage (anti-redirection to a different id).
1090            assert_ne!(
1091                base,
1092                identity_create_from_shielded_extra_sighash_data(
1093                    &id_b,
1094                    10_000_000_000,
1095                    &fallback,
1096                    &keys
1097                ),
1098                "identity id must be bound"
1099            );
1100            // Changing the denomination changes the preimage.
1101            assert_ne!(
1102                base,
1103                identity_create_from_shielded_extra_sighash_data(
1104                    &id_a,
1105                    25_000_000_000,
1106                    &fallback,
1107                    &keys
1108                ),
1109                "denomination must be bound"
1110            );
1111            // Changing the fallback failure address changes the preimage (anti-redirection of the
1112            // failure credit: a relayer cannot point the penalty-charged spend at a different
1113            // address than the one each key's proof-of-possession signed).
1114            assert_ne!(
1115                base,
1116                identity_create_from_shielded_extra_sighash_data(
1117                    &id_a,
1118                    10_000_000_000,
1119                    &PlatformAddress::P2pkh([0x02u8; 20]),
1120                    &keys
1121                ),
1122                "fallback failure address hash must be bound"
1123            );
1124            // Changing only the fallback address TYPE (P2pkh -> P2sh, same hash) changes the
1125            // preimage too (the type tag is bound, not just the hash).
1126            assert_ne!(
1127                base,
1128                identity_create_from_shielded_extra_sighash_data(
1129                    &id_a,
1130                    10_000_000_000,
1131                    &PlatformAddress::P2sh([0x01u8; 20]),
1132                    &keys
1133                ),
1134                "fallback failure address type tag must be bound"
1135            );
1136            // Swapping in a different key changes the preimage (anti-key-swap).
1137            assert_ne!(
1138                base,
1139                identity_create_from_shielded_extra_sighash_data(
1140                    &id_a,
1141                    10_000_000_000,
1142                    &fallback,
1143                    &[mk_key(0, 0xBB)]
1144                ),
1145                "key data must be bound"
1146            );
1147            // Adding a key changes the preimage (the full set is bound, not just the count).
1148            assert_ne!(
1149                base,
1150                identity_create_from_shielded_extra_sighash_data(
1151                    &id_a,
1152                    10_000_000_000,
1153                    &fallback,
1154                    &[mk_key(0, 0xAA), mk_key(1, 0xCC)]
1155                ),
1156                "the full key set must be bound"
1157            );
1158        }
1159
1160        #[test]
1161        fn binds_read_only_and_contract_bounds() {
1162            use crate::identity::identity_public_key::contract_bounds::ContractBounds;
1163            use crate::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Setters;
1164            let id = [0x11u8; 32];
1165            let fallback = PlatformAddress::P2pkh([0x01u8; 20]);
1166            let base = identity_create_from_shielded_extra_sighash_data(
1167                &id,
1168                10_000_000_000,
1169                &fallback,
1170                &[mk_key(0, 0xAA)],
1171            );
1172
1173            // Flipping read_only changes the preimage (un-malleable for every key type).
1174            let mut ro_key = mk_key(0, 0xAA);
1175            ro_key.set_read_only(true);
1176            assert_ne!(
1177                base,
1178                identity_create_from_shielded_extra_sighash_data(
1179                    &id,
1180                    10_000_000_000,
1181                    &fallback,
1182                    &[ro_key]
1183                ),
1184                "read_only must be bound"
1185            );
1186
1187            // Attaching contract_bounds changes the preimage.
1188            let mut cb_key = mk_key(0, 0xAA);
1189            cb_key.set_contract_bounds(Some(ContractBounds::SingleContract {
1190                id: platform_value::Identifier::new([0x33; 32]),
1191            }));
1192            assert_ne!(
1193                base,
1194                identity_create_from_shielded_extra_sighash_data(
1195                    &id,
1196                    10_000_000_000,
1197                    &fallback,
1198                    &[cb_key]
1199                ),
1200                "contract_bounds must be bound"
1201            );
1202        }
1203
1204        #[test]
1205        fn should_encode_the_reserved_contract_group_tag_at_the_end_of_the_key() {
1206            use crate::identity::identity_public_key::contract_bounds::ContractBounds;
1207            use crate::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Setters;
1208            // Consensus and the builder refuse a group-bound key before this preimage is built;
1209            // the arm exists so the encoder stays total. Pin what it writes.
1210            let mut key = mk_key(0, 0xAA);
1211            key.set_contract_bounds(Some(ContractBounds::ContractGroup {
1212                id: platform_value::Identifier::new([0x44; 32]),
1213            }));
1214            let data = identity_create_from_shielded_extra_sighash_data(
1215                &[0x11u8; 32],
1216                10_000_000_000,
1217                &PlatformAddress::P2pkh([0x01u8; 20]),
1218                &[key],
1219            );
1220            assert_eq!(data[data.len() - 33], 3);
1221            assert_eq!(&data[data.len() - 32..], &[0x44u8; 32]);
1222        }
1223    }
1224
1225    #[test]
1226    fn document_token_payment_layout_is_token_owner_contract_document_amount() {
1227        let data = document_token_payment_extra_sighash_data_v0(
1228            &[1u8; 32], &[2u8; 32], &[3u8; 32], &[4u8; 32], 10,
1229        );
1230        assert_eq!(data.len(), 136);
1231        assert_eq!(&data[..32], &[1u8; 32]);
1232        assert_eq!(&data[32..64], &[2u8; 32]);
1233        assert_eq!(&data[64..96], &[3u8; 32]);
1234        assert_eq!(&data[96..128], &[4u8; 32]);
1235        assert_eq!(&data[128..], &10u64.to_le_bytes());
1236    }
1237
1238    #[test]
1239    fn token_pool_paid_layouts_start_with_the_state_transition_type() {
1240        let transfer = token_shielded_transfer_with_shielded_fee_extra_sighash_data_v0(&[1u8; 32]);
1241        assert_eq!(transfer.len(), 33);
1242        assert_eq!(transfer[0], 26);
1243        assert_eq!(&transfer[1..], &[1u8; 32]);
1244
1245        let unshield =
1246            token_unshield_with_shielded_fee_extra_sighash_data_v0(&[1u8; 32], &[2u8; 32], 300);
1247        assert_eq!(unshield.len(), 73);
1248        assert_eq!(unshield[0], 27);
1249        assert_eq!(&unshield[1..33], &[1u8; 32]);
1250        assert_eq!(&unshield[33..65], &[2u8; 32]);
1251        assert_eq!(&unshield[65..], &300u64.to_le_bytes());
1252
1253        let purchase = token_purchase_from_shielded_pool_extra_sighash_data_v0(&[1u8; 32], 5, 900);
1254        assert_eq!(purchase.len(), 49);
1255        assert_eq!(purchase[0], 28);
1256        assert_eq!(&purchase[33..41], &5u64.to_le_bytes());
1257        assert_eq!(&purchase[41..], &900u64.to_le_bytes());
1258
1259        let fee = token_pool_fee_bundle_extra_sighash_data_v0(27, &[1u8; 32], &[3u8; 32]);
1260        assert_eq!(fee.len(), 65);
1261        assert_eq!(fee[0], 27);
1262        assert_eq!(&fee[1..33], &[1u8; 32]);
1263        assert_eq!(&fee[33..], &[3u8; 32]);
1264    }
1265
1266    #[test]
1267    fn token_burn_from_pool_layout_is_token_burner_amount() {
1268        let data = token_burn_from_pool_extra_sighash_data_v0(&[1u8; 32], &[2u8; 32], 300);
1269        assert_eq!(data.len(), 72);
1270        assert_eq!(&data[..32], &[1u8; 32]);
1271        assert_eq!(&data[32..64], &[2u8; 32]);
1272        assert_eq!(&data[64..], &300u64.to_le_bytes());
1273    }
1274
1275    #[test]
1276    fn outputs_only_token_pool_sighash_data_pins_its_v0_layout() {
1277        // Every byte here is consensus: a kind that changes its tag invalidates every bundle
1278        // already proved for it, so the four are pinned literally, not to the constants.
1279        let token_id = [7u8; 32];
1280        let owner_id = [5u8; 32];
1281        let version = PlatformVersion::latest();
1282        for (action_type, tag) in [
1283            (TokenTransitionActionType::Shield, 0x80u8),
1284            (TokenTransitionActionType::MintToPool, 0x81),
1285            (TokenTransitionActionType::ClaimToPool, 0x82),
1286            (TokenTransitionActionType::DirectPurchaseToPool, 0x83),
1287        ] {
1288            let mut expected = Vec::with_capacity(65);
1289            expected.push(tag);
1290            expected.extend_from_slice(&token_id);
1291            expected.extend_from_slice(&owner_id);
1292            assert_eq!(
1293                token_pool_output_only_extra_sighash_data(
1294                    action_type,
1295                    &token_id,
1296                    &owner_id,
1297                    version
1298                )
1299                .expect("outputs-only kind"),
1300                expected,
1301                "v0 layout is frozen for {action_type}: tag (1) || token_id (32) || owner_id (32)"
1302            );
1303        }
1304    }
1305
1306    #[test]
1307    fn outputs_only_token_pool_sighash_data_separates_pools() {
1308        // The whole point of the data: an outputs-only bundle carries no anchor, so without it
1309        // the same proved bytes verify against every token pool.
1310        let version = PlatformVersion::latest();
1311        let a = token_pool_output_only_extra_sighash_data(
1312            TokenTransitionActionType::Shield,
1313            &[1u8; 32],
1314            &[5u8; 32],
1315            version,
1316        )
1317        .expect("shield tag");
1318        let b = token_pool_output_only_extra_sighash_data(
1319            TokenTransitionActionType::Shield,
1320            &[2u8; 32],
1321            &[5u8; 32],
1322            version,
1323        )
1324        .expect("shield tag");
1325        assert_ne!(
1326            a, b,
1327            "a bundle must not verify against another token's pool"
1328        );
1329    }
1330
1331    #[test]
1332    fn outputs_only_token_pool_sighash_data_separates_transition_kinds() {
1333        // All four kinds bind the same token id and the same owner, so only the tag keeps a
1334        // shield bundle from being resubmitted by its owner as a mint, a claim or a purchase
1335        // into the very same pool.
1336        let version = PlatformVersion::latest();
1337        let token_id = [9u8; 32];
1338        let owner_id = [5u8; 32];
1339        let built: Vec<Vec<u8>> = [
1340            TokenTransitionActionType::Shield,
1341            TokenTransitionActionType::MintToPool,
1342            TokenTransitionActionType::ClaimToPool,
1343            TokenTransitionActionType::DirectPurchaseToPool,
1344        ]
1345        .into_iter()
1346        .map(|action_type| {
1347            token_pool_output_only_extra_sighash_data(action_type, &token_id, &owner_id, version)
1348                .expect("outputs-only kind")
1349        })
1350        .collect();
1351
1352        for (i, a) in built.iter().enumerate() {
1353            for b in built.iter().skip(i + 1) {
1354                assert_ne!(a, b, "each transition kind must get its own preimage");
1355            }
1356        }
1357    }
1358
1359    #[test]
1360    fn outputs_only_token_pool_sighash_data_refuses_every_other_kind() {
1361        // The kinds come from an enum shared with clients that carries fourteen other
1362        // variants. Handing one of those in is a caller bug, and it must be loud: silently
1363        // falling back to some default byte would hand two kinds the same preimage.
1364        let version = PlatformVersion::latest();
1365        // All fourteen, not a sample: match exhaustiveness protects against a kind being
1366        // *added* and forgotten, but nothing stops an existing arm being edited into the
1367        // accepting half, and a sample would not notice.
1368        for action_type in [
1369            TokenTransitionActionType::Burn,
1370            TokenTransitionActionType::Mint,
1371            TokenTransitionActionType::Transfer,
1372            TokenTransitionActionType::Freeze,
1373            TokenTransitionActionType::Unfreeze,
1374            TokenTransitionActionType::DestroyFrozenFunds,
1375            TokenTransitionActionType::Claim,
1376            TokenTransitionActionType::EmergencyAction,
1377            TokenTransitionActionType::ConfigUpdate,
1378            TokenTransitionActionType::DirectPurchase,
1379            TokenTransitionActionType::SetPriceForDirectPurchase,
1380            TokenTransitionActionType::BurnFromPool,
1381            TokenTransitionActionType::Unshield,
1382            TokenTransitionActionType::ShieldedTransfer,
1383        ] {
1384            let error = token_pool_output_only_extra_sighash_data(
1385                action_type,
1386                &[1u8; 32],
1387                &[5u8; 32],
1388                version,
1389            )
1390            .expect_err("only outputs-only pool bundles have a tag");
1391            assert!(
1392                matches!(error, ProtocolError::InvalidStateTransitionType(_)),
1393                "{action_type} must be refused, got {error:?}"
1394            );
1395        }
1396    }
1397
1398    #[test]
1399    fn outputs_only_token_pool_tags_cannot_collide_with_state_transition_types() {
1400        // The tags share a preimage slot with the `StateTransitionType` byte the identity-less
1401        // token bundles commit to, and one of those layouts — the credit pool fee bundle's
1402        // `type || token_id || token actions digest` — has the same 1 + 32 + 32 length, so a
1403        // colliding tag would make the two layouts byte-for-byte the same shape. Asking the enum
1404        // itself — rather than comparing against the three types that exist today — is what
1405        // makes this break on the day someone assigns a transition type inside the tag range.
1406        for tag in [
1407            TOKEN_SHIELD_BUNDLE_TAG,
1408            TOKEN_MINT_TO_POOL_BUNDLE_TAG,
1409            TOKEN_CLAIM_TO_POOL_BUNDLE_TAG,
1410            TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG,
1411        ] {
1412            assert!(
1413                StateTransitionType::try_from(tag).is_err(),
1414                "state transition type {tag:#04x} now collides with an outputs-only bundle tag"
1415            );
1416        }
1417    }
1418
1419    mod credit_pool_outputs_only {
1420        use super::*;
1421        use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof;
1422        use crate::identity::state_transition::asset_lock_proof::InstantAssetLockProof;
1423        use crate::util::hash::hash_double;
1424        use dashcore::transaction::special_transaction::asset_lock::AssetLockPayload;
1425        use dashcore::transaction::special_transaction::TransactionPayload;
1426        use dashcore::{InstantLock, OutPoint, ScriptBuf, Transaction, TxOut};
1427
1428        fn protocol_version(version: u32) -> &'static PlatformVersion {
1429            PlatformVersion::get(version).expect("known protocol version")
1430        }
1431
1432        fn chain_asset_lock_proof(outpoint: [u8; 36]) -> AssetLockProof {
1433            AssetLockProof::Chain(ChainAssetLockProof {
1434                core_chain_locked_height: 100,
1435                out_point: OutPoint::from(outpoint),
1436            })
1437        }
1438
1439        fn inputs(
1440            entries: &[(PlatformAddress, AddressNonce, Credits)],
1441        ) -> BTreeMap<PlatformAddress, (AddressNonce, Credits)> {
1442            entries
1443                .iter()
1444                .map(|(address, nonce, amount)| (*address, (*nonce, *amount)))
1445                .collect()
1446        }
1447
1448        #[test]
1449        fn should_pin_the_v0_layout_of_all_three_kinds() {
1450            // Every byte here is consensus: a kind whose tag or owner changes invalidates every
1451            // bundle already proved for it, so the tags are written literally, not through the
1452            // constants, and the owners are derived independently of the code under test.
1453            let version = PlatformVersion::latest();
1454
1455            // The map orders P2pkh before P2sh whatever their hash bytes, and each address is its
1456            // 21-byte encoding: variant index, then the 20-byte hash.
1457            let shield_inputs = inputs(&[
1458                (PlatformAddress::P2sh([0x00; 20]), 1, 10),
1459                (PlatformAddress::P2pkh([0x11; 20]), 2, 20),
1460            ]);
1461            let mut funding = Vec::new();
1462            funding.push(0x00);
1463            funding.extend_from_slice(&[0x11; 20]);
1464            funding.push(0x01);
1465            funding.extend_from_slice(&[0x00; 20]);
1466            let mut expected = vec![0x84];
1467            expected.extend_from_slice(&Sha256::digest(&funding));
1468            assert_eq!(
1469                shield_extra_sighash_data(&shield_inputs, version).expect("shield"),
1470                expected,
1471                "Shield: 0x84 || SHA-256 of the input addresses in map order"
1472            );
1473
1474            let identity_id = [0x22u8; 32];
1475            let mut expected = vec![0x85];
1476            expected.extend_from_slice(&identity_id);
1477            assert_eq!(
1478                shield_from_identity_extra_sighash_data(&identity_id, version)
1479                    .expect("shield from identity"),
1480                expected,
1481                "ShieldFromIdentity: 0x85 || identity id"
1482            );
1483
1484            let outpoint = [0x33u8; 36];
1485            let mut expected = vec![0x86];
1486            expected.extend_from_slice(&hash_double(outpoint));
1487            assert_eq!(
1488                shield_from_asset_lock_extra_sighash_data(
1489                    &chain_asset_lock_proof(outpoint),
1490                    version
1491                )
1492                .expect("shield from asset lock"),
1493                expected,
1494                "ShieldFromAssetLock: 0x86 || double SHA-256 of the locked outpoint"
1495            );
1496        }
1497
1498        #[test]
1499        fn should_bind_the_same_shield_funding_whatever_order_the_inputs_were_assembled_in() {
1500            // The funding digest hashes the addresses in the map's key order, and that order is
1501            // wire format: a client that collected the same addresses in another order must
1502            // still produce the bytes the verifier rebuilds from the transition.
1503            let version = PlatformVersion::latest();
1504            let a = (PlatformAddress::P2pkh([0x01; 20]), 1, 100);
1505            let b = (PlatformAddress::P2sh([0x02; 20]), 2, 200);
1506            let c = (PlatformAddress::P2pkh([0x03; 20]), 3, 300);
1507
1508            let forward = shield_extra_sighash_data(&inputs(&[a, b, c]), version).expect("shield");
1509            let backward = shield_extra_sighash_data(&inputs(&[c, b, a]), version).expect("shield");
1510            let shuffled = shield_extra_sighash_data(&inputs(&[b, c, a]), version).expect("shield");
1511
1512            assert_eq!(forward, backward);
1513            assert_eq!(forward, shuffled);
1514        }
1515
1516        #[test]
1517        fn should_bind_who_funds_a_shield_but_not_its_nonces_or_amounts() {
1518            // The owner is the funding, not the transition: the same addresses with other nonces
1519            // or other contributions bind the same bytes, so a later "tightening" that adds the
1520            // nonce shows up here as a layout change rather than slipping in.
1521            let version = PlatformVersion::latest();
1522            let first = PlatformAddress::P2pkh([0x01; 20]);
1523            let second = PlatformAddress::P2pkh([0x02; 20]);
1524            let base =
1525                shield_extra_sighash_data(&inputs(&[(first, 1, 100)]), version).expect("shield");
1526
1527            assert_eq!(
1528                base,
1529                shield_extra_sighash_data(&inputs(&[(first, 9, 100)]), version).expect("shield"),
1530                "the nonce must not be bound"
1531            );
1532            assert_eq!(
1533                base,
1534                shield_extra_sighash_data(&inputs(&[(first, 1, 999)]), version).expect("shield"),
1535                "the contributed amount must not be bound"
1536            );
1537            assert_ne!(
1538                base,
1539                shield_extra_sighash_data(&inputs(&[(second, 1, 100)]), version).expect("shield"),
1540                "another funding address is another owner"
1541            );
1542            assert_ne!(
1543                base,
1544                shield_extra_sighash_data(&inputs(&[(first, 1, 100), (second, 1, 100)]), version)
1545                    .expect("shield"),
1546                "an added funding address is another owner"
1547            );
1548            assert_ne!(
1549                base,
1550                shield_extra_sighash_data(
1551                    &inputs(&[(PlatformAddress::P2sh([0x01; 20]), 1, 100)]),
1552                    version
1553                )
1554                .expect("shield"),
1555                "the address type is part of the owner, not only its hash"
1556            );
1557        }
1558
1559        #[test]
1560        fn should_keep_kinds_apart_when_their_owners_coincide() {
1561            // An identity created from an asset lock has that lock's identifier as its id, so a
1562            // `ShieldFromAssetLock` and a `ShieldFromIdentity` can bind the very same owner bytes.
1563            // Only the tag keeps a bundle proved for one from being accepted as the other.
1564            let version = PlatformVersion::latest();
1565            let proof = chain_asset_lock_proof([0x44; 36]);
1566            let identity_id = proof.create_identifier().expect("identifier").to_buffer();
1567
1568            let from_lock =
1569                shield_from_asset_lock_extra_sighash_data(&proof, version).expect("asset lock");
1570            let from_identity =
1571                shield_from_identity_extra_sighash_data(&identity_id, version).expect("identity");
1572
1573            assert_eq!(&from_lock[1..], &from_identity[1..], "the owners coincide");
1574            assert_ne!(from_lock, from_identity, "the kinds must not");
1575        }
1576
1577        #[test]
1578        fn should_bind_the_whole_outpoint_of_the_asset_lock_not_only_its_transaction() {
1579            // A lock transaction can carry several credit outputs, each its own asset lock. Bound
1580            // to the transaction id alone, a bundle could be lifted by whoever holds another
1581            // output of the same transaction. The owner is the outpoint, and it is the same owner
1582            // whichever kind of proof presents it.
1583            let version = PlatformVersion::latest();
1584            let credit_output = |value| TxOut {
1585                value,
1586                script_pubkey: ScriptBuf::new(),
1587            };
1588            let transaction = Transaction {
1589                version: 3,
1590                lock_time: 0,
1591                input: vec![],
1592                output: vec![],
1593                special_transaction_payload: Some(TransactionPayload::AssetLockPayloadType(
1594                    AssetLockPayload {
1595                        version: 0,
1596                        credit_outputs: vec![credit_output(100_000), credit_output(200_000)],
1597                    },
1598                )),
1599            };
1600            let txid = transaction.txid();
1601            let instant = |output_index| {
1602                AssetLockProof::Instant(InstantAssetLockProof::new(
1603                    InstantLock::default(),
1604                    transaction.clone(),
1605                    output_index,
1606                ))
1607            };
1608            let bound = |proof: &AssetLockProof| {
1609                shield_from_asset_lock_extra_sighash_data(proof, version).expect("asset lock")
1610            };
1611
1612            assert_ne!(
1613                bound(&instant(0)),
1614                bound(&instant(1)),
1615                "another output of the same lock transaction is another owner"
1616            );
1617            assert_eq!(
1618                bound(&instant(1)),
1619                bound(&AssetLockProof::Chain(ChainAssetLockProof {
1620                    core_chain_locked_height: 100,
1621                    out_point: OutPoint::new(txid, 1),
1622                })),
1623                "the same outpoint is the same owner whichever proof presents it"
1624            );
1625        }
1626
1627        #[test]
1628        fn should_bind_nothing_at_protocol_versions_before_the_binding() {
1629            // Protocol versions 12 and 13 run the credit pool with verifiers that rebuild an empty
1630            // preimage. A client building for one of them reads the same field, so it must get the
1631            // empty preimage back, or every shield it makes there is rejected.
1632            let lock = chain_asset_lock_proof([0x55; 36]);
1633            let funding = inputs(&[(PlatformAddress::P2pkh([0x01; 20]), 1, 100)]);
1634            for version in [12, 13] {
1635                let version = protocol_version(version);
1636                assert_eq!(version.dpp.methods.credit_pool_bundle_binding, None);
1637                assert!(shield_extra_sighash_data(&funding, version)
1638                    .expect("shield")
1639                    .is_empty());
1640                assert!(
1641                    shield_from_identity_extra_sighash_data(&[0x66; 32], version)
1642                        .expect("shield from identity")
1643                        .is_empty()
1644                );
1645                assert!(shield_from_asset_lock_extra_sighash_data(&lock, version)
1646                    .expect("shield from asset lock")
1647                    .is_empty());
1648            }
1649        }
1650
1651        #[test]
1652        fn should_refuse_an_unknown_binding_version() {
1653            // Neither side may guess: a builder that fell back to one layout while the verifier
1654            // chose another would reject every honest shield.
1655            let mut version = PlatformVersion::latest().clone();
1656            version.dpp.methods.credit_pool_bundle_binding = Some(1);
1657            let lock = chain_asset_lock_proof([0x77; 36]);
1658
1659            for result in [
1660                shield_extra_sighash_data(&BTreeMap::new(), &version),
1661                shield_from_identity_extra_sighash_data(&[0x88; 32], &version),
1662                shield_from_asset_lock_extra_sighash_data(&lock, &version),
1663            ] {
1664                assert_matches::assert_matches!(
1665                    result,
1666                    Err(ProtocolError::UnknownVersionMismatch { received: 1, .. })
1667                );
1668            }
1669        }
1670
1671        #[test]
1672        fn credit_pool_outputs_only_tags_cannot_collide_with_state_transition_types() {
1673            // The tags share a preimage slot with the `StateTransitionType` byte the identity-less
1674            // token bundles commit to, at the same 1 + 32 length. Asking the enum itself is what
1675            // makes this break on the day a transition type is assigned inside the tag range.
1676            for tag in [
1677                SHIELD_BUNDLE_TAG,
1678                SHIELD_FROM_IDENTITY_BUNDLE_TAG,
1679                SHIELD_FROM_ASSET_LOCK_BUNDLE_TAG,
1680            ] {
1681                assert!(
1682                    StateTransitionType::try_from(tag).is_err(),
1683                    "state transition type {tag:#04x} now collides with a credit pool bundle tag"
1684                );
1685            }
1686        }
1687
1688        #[test]
1689        fn outputs_only_bundle_tags_are_pairwise_distinct() {
1690            // All seven outputs-only layouts are `tag (1) || 32 bytes`, so two kinds sharing a
1691            // tag would share a preimage whenever their 32 bytes coincide.
1692            let tags = [
1693                TOKEN_SHIELD_BUNDLE_TAG,
1694                TOKEN_MINT_TO_POOL_BUNDLE_TAG,
1695                TOKEN_CLAIM_TO_POOL_BUNDLE_TAG,
1696                TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG,
1697                SHIELD_BUNDLE_TAG,
1698                SHIELD_FROM_IDENTITY_BUNDLE_TAG,
1699                SHIELD_FROM_ASSET_LOCK_BUNDLE_TAG,
1700            ];
1701            for (i, a) in tags.iter().enumerate() {
1702                for b in tags.iter().skip(i + 1) {
1703                    assert_ne!(a, b, "outputs-only bundle tag {a:#04x} is used twice");
1704                }
1705            }
1706        }
1707    }
1708}