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`.
8
9use crate::address_funds::PlatformAddress;
10use crate::identity::identity_public_key::contract_bounds::ContractBounds;
11use crate::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Getters;
12use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation;
13use crate::withdrawal::Pooling;
14use crate::ProtocolError;
15use platform_version::version::PlatformVersion;
16use sha2::{Digest, Sha256};
17
18/// Domain separator for Platform sighash computation.
19const SIGHASH_DOMAIN: &[u8] = b"DashPlatformSighash";
20
21/// Computes the platform sighash from an Orchard bundle commitment and optional
22/// transparent field data.
23///
24/// The sighash is computed as:
25///   `SHA-256(SIGHASH_DOMAIN || bundle_commitment || extra_data)`
26///
27/// This binds transparent state transition fields (like `output_address` in unshield
28/// or `output_script` in shielded withdrawal) to the Orchard signatures, preventing
29/// replay attacks where an attacker substitutes transparent fields while reusing a
30/// valid Orchard bundle.
31///
32/// The same computation must be used on both the signing (client) and verification
33/// (platform) sides. For transitions without transparent fields (shield and
34/// shielded_transfer), `extra_data` is empty.
35pub fn compute_platform_sighash(bundle_commitment: &[u8; 32], extra_data: &[u8]) -> [u8; 32] {
36    let mut hasher = Sha256::new();
37    hasher.update(SIGHASH_DOMAIN);
38    hasher.update(bundle_commitment);
39    hasher.update(extra_data);
40    hasher.finalize().into()
41}
42
43/// Builds the transparent `extra_data` bound into a ShieldedWithdrawal's platform
44/// sighash, with the byte layout
45/// `output_script || unshielding_amount (u64 LE) || core_fee_per_byte (u32 LE) || pooling (u8)`.
46///
47/// Every field here is written verbatim by the transformer into the queued withdrawal
48/// document that constructs the Core asset-unlock TxOut. Binding all of them into the
49/// Orchard sighash means the binding signature authorizes them: since ShieldedWithdrawal
50/// has no identity-key signature and no address-witness check, the Orchard signature is
51/// the only authorization boundary, so a relay or block proposer cannot malleate
52/// `core_fee_per_byte` (or `pooling`, were it ever unpinned from `Never`) — e.g. flip a
53/// user's `core_fee_per_byte = 1` to a much larger Fibonacci value to redirect the
54/// withdrawn amount into L1 miner fees — without invalidating the proof.
55///
56/// The signing (client/builder) and verifying (consensus) sides MUST produce identical
57/// bytes, so both call this single function.
58///
59/// The layout places the variable-length `output_script` first with no length prefix. This
60/// is unambiguous only because `validate_structure` runs before proof verification and pins
61/// `output_script` to a canonical, fixed-length P2PKH (25 bytes) or P2SH (23 bytes); the
62/// remaining fields are fixed-width, so the preimage is well-defined for every accepted
63/// transition. If that script-shape restriction is ever relaxed, add a length prefix here.
64/// Dispatches on the platform-versioned `dpp.methods.shielded_extra_sighash_data` so the
65/// consensus-critical byte layout can evolve across protocol versions without breaking older
66/// transitions — the same versioning the sibling shielded fee methods use. The signing
67/// (client/builder) and verifying (consensus) sides both call this single function with the same
68/// `platform_version`, so they can never produce divergent preimages.
69pub fn shielded_withdrawal_extra_sighash_data(
70    output_script: &[u8],
71    unshielding_amount: u64,
72    core_fee_per_byte: u32,
73    pooling: Pooling,
74    platform_version: &PlatformVersion,
75) -> Result<Vec<u8>, ProtocolError> {
76    match platform_version.dpp.methods.shielded_extra_sighash_data {
77        0 => Ok(shielded_withdrawal_extra_sighash_data_v0(
78            output_script,
79            unshielding_amount,
80            core_fee_per_byte,
81            pooling,
82        )),
83        version => Err(ProtocolError::UnknownVersionMismatch {
84            method: "shielded_withdrawal_extra_sighash_data".to_string(),
85            known_versions: vec![0],
86            received: version,
87        }),
88    }
89}
90
91/// v0 byte layout of [`shielded_withdrawal_extra_sighash_data`] (see that function's doc comment for
92/// the layout and rationale). Frozen: never mutate; a layout change requires a new `_v1` + version.
93pub fn shielded_withdrawal_extra_sighash_data_v0(
94    output_script: &[u8],
95    unshielding_amount: u64,
96    core_fee_per_byte: u32,
97    pooling: Pooling,
98) -> Vec<u8> {
99    let mut data = Vec::with_capacity(output_script.len() + 8 + 4 + 1);
100    data.extend_from_slice(output_script);
101    data.extend_from_slice(&unshielding_amount.to_le_bytes());
102    data.extend_from_slice(&core_fee_per_byte.to_le_bytes());
103    data.push(pooling as u8);
104    data
105}
106
107/// Builds the transparent `extra_data` bound into an Unshield's platform sighash, with the
108/// byte layout `output_address || unshielding_amount (u64 LE)`.
109///
110/// As with [`shielded_withdrawal_extra_sighash_data`], the signing (client/builder) and
111/// verifying (consensus) sides MUST produce identical bytes, so both call this single
112/// function. Unshield credits a transparent platform address (not a Core asset-unlock
113/// `TxOut`), so it carries no `core_fee_per_byte`/`pooling` to bind.
114pub fn unshield_extra_sighash_data(
115    output_address: &[u8],
116    unshielding_amount: u64,
117    platform_version: &PlatformVersion,
118) -> Result<Vec<u8>, ProtocolError> {
119    match platform_version.dpp.methods.shielded_extra_sighash_data {
120        0 => Ok(unshield_extra_sighash_data_v0(
121            output_address,
122            unshielding_amount,
123        )),
124        version => Err(ProtocolError::UnknownVersionMismatch {
125            method: "unshield_extra_sighash_data".to_string(),
126            known_versions: vec![0],
127            received: version,
128        }),
129    }
130}
131
132/// v0 byte layout of [`unshield_extra_sighash_data`] (see that function's doc comment for the layout
133/// and rationale). Frozen: never mutate; a layout change requires a new `_v1` + version bump.
134pub fn unshield_extra_sighash_data_v0(output_address: &[u8], unshielding_amount: u64) -> Vec<u8> {
135    let mut data = Vec::with_capacity(output_address.len() + 8);
136    data.extend_from_slice(output_address);
137    data.extend_from_slice(&unshielding_amount.to_le_bytes());
138    data
139}
140
141/// Builds the transparent `extra_data` bound into an `IdentityTopUpFromShieldedPool`'s platform
142/// sighash, with the byte layout `identity_id (32) || top_up_amount (u64 LE)`.
143///
144/// Like `Unshield`, the transition carries no platform signature, so the state-determining
145/// transparent fields (which identity is credited, and the gross amount leaving the pool) must be
146/// committed into the Orchard binding sighash; otherwise a relayer could take a valid spend bundle
147/// and re-point it at a different identity. The client builder and the consensus verifier both
148/// call this single function.
149pub fn identity_top_up_from_shielded_extra_sighash_data(
150    identity_id: &[u8; 32],
151    top_up_amount: u64,
152    platform_version: &PlatformVersion,
153) -> Result<Vec<u8>, ProtocolError> {
154    match platform_version.dpp.methods.shielded_extra_sighash_data {
155        0 => Ok(identity_top_up_from_shielded_extra_sighash_data_v0(
156            identity_id,
157            top_up_amount,
158        )),
159        version => Err(ProtocolError::UnknownVersionMismatch {
160            method: "identity_top_up_from_shielded_extra_sighash_data".to_string(),
161            known_versions: vec![0],
162            received: version,
163        }),
164    }
165}
166
167/// v0 byte layout of [`identity_top_up_from_shielded_extra_sighash_data`]. Frozen: never mutate;
168/// a layout change requires a new `_v1` + version bump.
169pub fn identity_top_up_from_shielded_extra_sighash_data_v0(
170    identity_id: &[u8; 32],
171    top_up_amount: u64,
172) -> Vec<u8> {
173    let mut data = Vec::with_capacity(32 + 8);
174    data.extend_from_slice(identity_id);
175    data.extend_from_slice(&top_up_amount.to_le_bytes());
176    data
177}
178
179/// Builds the transparent `extra_data` bound into an `IdentityCreateFromShieldedPool`'s platform
180/// sighash, with the byte layout
181/// `identity_id (32) || denomination (u64 LE)
182///   || send_to_address_on_creation_failure (tag u8: 0=P2pkh, 1=P2sh || hash 20)
183///   || num_keys (u16 LE)
184///   || for each key in supplied order: key_id (u32 LE) || purpose (u8) || security_level (u8)
185///   || key_type (u8) || key_data_len (u16 LE) || key_data || read_only (u8)
186///   || contract_bounds (tag u8: 0=None, 1=SingleContract id(32), 2=SingleContractDocumentType
187///   id(32) name_len(u16 LE) name, 3=ContractGroup id(32))`.
188///
189/// Tag 3 is never reached: `IdentityCreateFromShieldedPool` refuses a key bound to a contract
190/// group before this preimage is built (consensus in `validate_shielded_proof` v1, the builder
191/// up front). The arm only keeps the encoder total without a panic on a block-execution path,
192/// so the v0 bytes of every reachable input are unchanged.
193///
194/// The budget and the expiry of a version 1 key are not in the layout either, and for the same
195/// reason never need to be: a key that carries either is refused at the same two places, so
196/// every key that reaches this preimage is fully described by the fields above. A version 1 key
197/// without limits binds the same bytes as its version 0 equivalent.
198///
199/// `IdentityCreateFromShieldedPool` carries NO platform identity signature: authorization is 100%
200/// the Orchard proof + per-action spend-auth signatures + binding signature over this sighash. The
201/// transparent, state-determining fields — the new identity id, the exit denomination, and the
202/// FULL public-key set — must therefore be committed into the Orchard sighash, exactly as the
203/// `surplus_output` field is committed into `ShieldFromAssetLock`'s ECDSA signature. Without this
204/// binding a relay or block proposer could take a valid bundle exiting a denomination and re-point
205/// it at a DIFFERENT identity id, or swap in DIFFERENT keys they control, stealing the credited
206/// balance (the per-key proofs-of-possession alone do NOT prevent this — a relayer keeps valid PoP
207/// sigs for their own keys while swapping the bundle). Binding `(this spend → these exact keys →
208/// this id → this denomination)` here makes the redirection atomic-or-invalid.
209///
210/// The signing (client/builder) and verifying (consensus) sides MUST produce identical bytes, so
211/// both call this single function. Unlike the fixed-length withdrawal/unshield helpers, the
212/// variable-length key list is fully length-prefixed (both the key count and each key's data) so
213/// the preimage is unambiguous for any key set.
214pub fn identity_create_from_shielded_extra_sighash_data(
215    identity_id: &[u8; 32],
216    denomination: u64,
217    send_to_address_on_creation_failure: &PlatformAddress,
218    public_keys: &[IdentityPublicKeyInCreation],
219    platform_version: &PlatformVersion,
220) -> Result<Vec<u8>, ProtocolError> {
221    match platform_version.dpp.methods.shielded_extra_sighash_data {
222        0 => Ok(identity_create_from_shielded_extra_sighash_data_v0(
223            identity_id,
224            denomination,
225            send_to_address_on_creation_failure,
226            public_keys,
227        )),
228        version => Err(ProtocolError::UnknownVersionMismatch {
229            method: "identity_create_from_shielded_extra_sighash_data".to_string(),
230            known_versions: vec![0],
231            received: version,
232        }),
233    }
234}
235
236/// v0 byte layout of [`identity_create_from_shielded_extra_sighash_data`] (see that function's doc
237/// comment for the layout and rationale). Frozen: never mutate; a layout change requires a new `_v1`
238/// + version bump.
239pub fn identity_create_from_shielded_extra_sighash_data_v0(
240    identity_id: &[u8; 32],
241    denomination: u64,
242    send_to_address_on_creation_failure: &PlatformAddress,
243    public_keys: &[IdentityPublicKeyInCreation],
244) -> Vec<u8> {
245    let mut data = Vec::with_capacity(32 + 8 + 21 + 2 + public_keys.len() * 44);
246    data.extend_from_slice(identity_id);
247    data.extend_from_slice(&denomination.to_le_bytes());
248    // Bind the fallback address (type tag || 20-byte hash) so a relayer cannot redirect the
249    // failure credit. Mirrors the way `unshield`/`withdrawal` bind their output address.
250    match send_to_address_on_creation_failure {
251        PlatformAddress::P2pkh(hash) => {
252            data.push(0u8);
253            data.extend_from_slice(hash);
254        }
255        PlatformAddress::P2sh(hash) => {
256            data.push(1u8);
257            data.extend_from_slice(hash);
258        }
259    }
260    data.extend_from_slice(&(public_keys.len() as u16).to_le_bytes());
261    for key in public_keys {
262        data.extend_from_slice(&key.id().to_le_bytes());
263        data.push(key.purpose() as u8);
264        data.push(key.security_level() as u8);
265        data.push(key.key_type() as u8);
266        let key_data = key.data().as_slice();
267        data.extend_from_slice(&(key_data.len() as u16).to_le_bytes());
268        data.extend_from_slice(key_data);
269        // Also bind `read_only` and `contract_bounds`. These are state-determining key fields that
270        // ARE in the transition's signable_bytes, but the per-key proof-of-possession does NOT bind
271        // them for hash-based key types (which accept an empty signature). Committing them into the
272        // Orchard binding sighash makes them un-malleable for EVERY key type, so a relayer/proposer
273        // cannot flip `read_only` or alter `contract_bounds` on an observed transition.
274        data.push(key.read_only() as u8);
275        match key.contract_bounds() {
276            None => data.push(0u8),
277            Some(ContractBounds::SingleContract { id }) => {
278                data.push(1u8);
279                data.extend_from_slice(id.as_bytes());
280            }
281            Some(ContractBounds::SingleContractDocumentType {
282                id,
283                document_type_name,
284            }) => {
285                data.push(2u8);
286                data.extend_from_slice(id.as_bytes());
287                let name = document_type_name.as_bytes();
288                data.extend_from_slice(&(name.len() as u16).to_le_bytes());
289                data.extend_from_slice(name);
290            }
291            Some(ContractBounds::ContractGroup { id }) => {
292                // Unreachable: refused before the preimage is built (see the layout doc).
293                data.push(3u8);
294                data.extend_from_slice(id.as_bytes());
295            }
296        }
297    }
298    data
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use crate::identity::core_script::CoreScript;
305    use crate::withdrawal::Pooling;
306    // These tests pin the v0 preimage directly (they assert exact bytes), so resolve the bare helper
307    // names to the `_v0` impls rather than the version-dispatching public wrappers.
308    use crate::shielded::shielded_withdrawal_extra_sighash_data_v0 as shielded_withdrawal_extra_sighash_data;
309    use crate::shielded::unshield_extra_sighash_data_v0 as unshield_extra_sighash_data;
310
311    #[test]
312    fn withdrawal_sighash_data_binds_core_fee_per_byte() {
313        let script = CoreScript::new_p2pkh([1u8; 20]);
314        let a = shielded_withdrawal_extra_sighash_data(script.as_bytes(), 1000, 1, Pooling::Never);
315        let b = shielded_withdrawal_extra_sighash_data(script.as_bytes(), 1000, 2, Pooling::Never);
316        assert_ne!(
317            a, b,
318            "changing core_fee_per_byte must change the sighash preimage"
319        );
320    }
321
322    #[test]
323    fn withdrawal_sighash_data_binds_pooling() {
324        // `pooling` is pinned to `Never` by `validate_structure`, so this binding is currently
325        // dead defense-in-depth; assert it is nonetheless mixed into the preimage so a future
326        // unpinning would still be authorized by the Orchard binding signature.
327        let script = CoreScript::new_p2pkh([1u8; 20]);
328        let a = shielded_withdrawal_extra_sighash_data(script.as_bytes(), 1000, 1, Pooling::Never);
329        let b = shielded_withdrawal_extra_sighash_data(
330            script.as_bytes(),
331            1000,
332            1,
333            Pooling::IfAvailable,
334        );
335        assert_ne!(a, b, "changing pooling must change the sighash preimage");
336    }
337
338    #[test]
339    fn withdrawal_sighash_data_layout() {
340        // output_script(2) || unshielding_amount(8) || core_fee_per_byte(4) || pooling(1)
341        let d = shielded_withdrawal_extra_sighash_data(&[0xAA, 0xBB], 1, 2, Pooling::Never);
342        assert_eq!(d.len(), 2 + 8 + 4 + 1);
343        assert_eq!(&d[0..2], &[0xAA, 0xBB]);
344        assert_eq!(&d[2..10], &1u64.to_le_bytes());
345        assert_eq!(&d[10..14], &2u32.to_le_bytes());
346        assert_eq!(d[14], Pooling::Never as u8);
347    }
348
349    #[test]
350    fn unshield_sighash_data_layout() {
351        // output_address || unshielding_amount(8)
352        let d = unshield_extra_sighash_data(&[0xAA, 0xBB, 0xCC], 5);
353        assert_eq!(d.len(), 3 + 8);
354        assert_eq!(&d[0..3], &[0xAA, 0xBB, 0xCC]);
355        assert_eq!(&d[3..11], &5u64.to_le_bytes());
356    }
357
358    mod identity_create_sighash {
359        use super::*;
360        // Pin the v0 preimage directly (see the note in the parent test module).
361        use crate::identity::{KeyType, Purpose, SecurityLevel};
362        use crate::shielded::identity_create_from_shielded_extra_sighash_data_v0 as identity_create_from_shielded_extra_sighash_data;
363        use crate::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0;
364        use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation;
365        use platform_value::BinaryData;
366
367        fn mk_key(id: u32, data_byte: u8) -> IdentityPublicKeyInCreation {
368            IdentityPublicKeyInCreation::V0(IdentityPublicKeyInCreationV0 {
369                id,
370                key_type: KeyType::ECDSA_SECP256K1,
371                purpose: Purpose::AUTHENTICATION,
372                security_level: SecurityLevel::MASTER,
373                contract_bounds: None,
374                read_only: false,
375                data: BinaryData::new(vec![data_byte; 33]),
376                signature: BinaryData::new(vec![]),
377            })
378        }
379
380        #[test]
381        fn layout_is_length_prefixed() {
382            // identity_id(32) || denomination(8)
383            //   || send_to_address_on_creation_failure (tag(1) || hash(20))
384            //   || num_keys(2)
385            //   || [key_id(4)|purpose|sec|type|len(2)|data|read_only(1)|contract_bounds_tag(1)]
386            let id = [0x11u8; 32];
387            let keys = vec![mk_key(7, 0xAB)];
388            let fallback = PlatformAddress::P2pkh([0x5Cu8; 20]);
389            let d = identity_create_from_shielded_extra_sighash_data(
390                &id,
391                10_000_000_000,
392                &fallback,
393                &keys,
394            );
395            assert_eq!(&d[0..32], &id);
396            assert_eq!(&d[32..40], &10_000_000_000u64.to_le_bytes());
397            // Fallback address: tag(0=P2pkh) at offset 40, 20-byte hash at 41..61.
398            assert_eq!(d[40], 0u8, "fallback address P2pkh tag");
399            assert_eq!(&d[41..61], &[0x5Cu8; 20], "fallback address hash");
400            assert_eq!(&d[61..63], &1u16.to_le_bytes());
401            assert_eq!(&d[63..67], &7u32.to_le_bytes());
402            assert_eq!(d[67], Purpose::AUTHENTICATION as u8);
403            assert_eq!(d[68], SecurityLevel::MASTER as u8);
404            assert_eq!(d[69], KeyType::ECDSA_SECP256K1 as u8);
405            assert_eq!(&d[70..72], &33u16.to_le_bytes());
406            assert_eq!(&d[72..105], &[0xAB; 33]);
407            assert_eq!(d[105], 0u8, "read_only=false");
408            assert_eq!(d[106], 0u8, "contract_bounds=None tag");
409            assert_eq!(d.len(), 32 + 8 + 21 + 2 + (4 + 1 + 1 + 1 + 2 + 33 + 1 + 1));
410        }
411
412        #[test]
413        fn binds_identity_id_denomination_and_keys() {
414            let id_a = [0x11u8; 32];
415            let id_b = [0x22u8; 32];
416            let keys = vec![mk_key(0, 0xAA)];
417            let fallback = PlatformAddress::P2pkh([0x01u8; 20]);
418            let base = identity_create_from_shielded_extra_sighash_data(
419                &id_a,
420                10_000_000_000,
421                &fallback,
422                &keys,
423            );
424
425            // Changing the identity id changes the preimage (anti-redirection to a different id).
426            assert_ne!(
427                base,
428                identity_create_from_shielded_extra_sighash_data(
429                    &id_b,
430                    10_000_000_000,
431                    &fallback,
432                    &keys
433                ),
434                "identity id must be bound"
435            );
436            // Changing the denomination changes the preimage.
437            assert_ne!(
438                base,
439                identity_create_from_shielded_extra_sighash_data(
440                    &id_a,
441                    25_000_000_000,
442                    &fallback,
443                    &keys
444                ),
445                "denomination must be bound"
446            );
447            // Changing the fallback failure address changes the preimage (anti-redirection of the
448            // failure credit: a relayer cannot point the penalty-charged spend at a different
449            // address than the one each key's proof-of-possession signed).
450            assert_ne!(
451                base,
452                identity_create_from_shielded_extra_sighash_data(
453                    &id_a,
454                    10_000_000_000,
455                    &PlatformAddress::P2pkh([0x02u8; 20]),
456                    &keys
457                ),
458                "fallback failure address hash must be bound"
459            );
460            // Changing only the fallback address TYPE (P2pkh -> P2sh, same hash) changes the
461            // preimage too (the type tag is bound, not just the hash).
462            assert_ne!(
463                base,
464                identity_create_from_shielded_extra_sighash_data(
465                    &id_a,
466                    10_000_000_000,
467                    &PlatformAddress::P2sh([0x01u8; 20]),
468                    &keys
469                ),
470                "fallback failure address type tag must be bound"
471            );
472            // Swapping in a different key changes the preimage (anti-key-swap).
473            assert_ne!(
474                base,
475                identity_create_from_shielded_extra_sighash_data(
476                    &id_a,
477                    10_000_000_000,
478                    &fallback,
479                    &[mk_key(0, 0xBB)]
480                ),
481                "key data must be bound"
482            );
483            // Adding a key changes the preimage (the full set is bound, not just the count).
484            assert_ne!(
485                base,
486                identity_create_from_shielded_extra_sighash_data(
487                    &id_a,
488                    10_000_000_000,
489                    &fallback,
490                    &[mk_key(0, 0xAA), mk_key(1, 0xCC)]
491                ),
492                "the full key set must be bound"
493            );
494        }
495
496        #[test]
497        fn binds_read_only_and_contract_bounds() {
498            use crate::identity::identity_public_key::contract_bounds::ContractBounds;
499            use crate::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Setters;
500            let id = [0x11u8; 32];
501            let fallback = PlatformAddress::P2pkh([0x01u8; 20]);
502            let base = identity_create_from_shielded_extra_sighash_data(
503                &id,
504                10_000_000_000,
505                &fallback,
506                &[mk_key(0, 0xAA)],
507            );
508
509            // Flipping read_only changes the preimage (un-malleable for every key type).
510            let mut ro_key = mk_key(0, 0xAA);
511            ro_key.set_read_only(true);
512            assert_ne!(
513                base,
514                identity_create_from_shielded_extra_sighash_data(
515                    &id,
516                    10_000_000_000,
517                    &fallback,
518                    &[ro_key]
519                ),
520                "read_only must be bound"
521            );
522
523            // Attaching contract_bounds changes the preimage.
524            let mut cb_key = mk_key(0, 0xAA);
525            cb_key.set_contract_bounds(Some(ContractBounds::SingleContract {
526                id: platform_value::Identifier::new([0x33; 32]),
527            }));
528            assert_ne!(
529                base,
530                identity_create_from_shielded_extra_sighash_data(
531                    &id,
532                    10_000_000_000,
533                    &fallback,
534                    &[cb_key]
535                ),
536                "contract_bounds must be bound"
537            );
538        }
539
540        #[test]
541        fn should_encode_the_reserved_contract_group_tag_at_the_end_of_the_key() {
542            use crate::identity::identity_public_key::contract_bounds::ContractBounds;
543            use crate::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Setters;
544            // Consensus and the builder refuse a group-bound key before this preimage is built;
545            // the arm exists so the encoder stays total. Pin what it writes.
546            let mut key = mk_key(0, 0xAA);
547            key.set_contract_bounds(Some(ContractBounds::ContractGroup {
548                id: platform_value::Identifier::new([0x44; 32]),
549            }));
550            let data = identity_create_from_shielded_extra_sighash_data(
551                &[0x11u8; 32],
552                10_000_000_000,
553                &PlatformAddress::P2pkh([0x01u8; 20]),
554                &[key],
555            );
556            assert_eq!(data[data.len() - 33], 3);
557            assert_eq!(&data[data.len() - 32..], &[0x44u8; 32]);
558        }
559    }
560}