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}