Skip to main content

dpp/state_transition/
mod.rs

1use derive_more::From;
2#[cfg(feature = "serde-conversion")]
3use serde::{Deserialize, Serialize};
4use state_transitions::document::batch_transition::batched_transition::document_transition::DocumentTransition;
5use std::collections::BTreeMap;
6use std::ops::RangeInclusive;
7
8use platform_value::{BinaryData, Identifier};
9pub use state_transition_types::*;
10
11use bincode::{Decode, DecodeUntrusted, Encode};
12#[cfg(any(
13    feature = "state-transition-signing",
14    feature = "state-transition-validation"
15))]
16use dashcore::signer;
17#[cfg(feature = "state-transition-validation")]
18use dashcore::signer::double_sha;
19use platform_serialization_derive::{
20    PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, PlatformSignable,
21};
22use platform_version::version::{PlatformVersion, ProtocolVersion, ALL_VERSIONS, LATEST_VERSION};
23
24#[cfg(any(
25    feature = "state-transition-signing",
26    feature = "state-transition-validation"
27))]
28use crate::BlsModule;
29use crate::ProtocolError;
30
31mod state_transition_types;
32
33pub mod state_transition_factory;
34
35pub mod errors;
36#[cfg(feature = "state-transition-signing")]
37use crate::util::hash::ripemd160_sha256;
38use crate::util::hash::{hash_double_to_vec, hash_single};
39
40pub mod proof_result;
41mod serialization;
42pub mod state_transitions;
43mod traits;
44#[cfg(feature = "state-transition-validation")]
45mod verify_identity_signed_signature;
46
47// pub mod state_transition_fee;
48
49#[cfg(feature = "state-transition-validation")]
50use crate::consensus::basic::UnsupportedFeatureError;
51#[cfg(feature = "state-transition-signing")]
52use crate::consensus::signature::InvalidSignaturePublicKeySecurityLevelError;
53#[cfg(feature = "state-transition-signing")]
54use crate::consensus::signature::{
55    ContractBoundedKeyNonBatchError, ContractBoundedKeyOutOfBoundsError,
56};
57#[cfg(feature = "state-transition-validation")]
58use crate::consensus::signature::{InvalidStateTransitionSignatureError, SignatureError};
59#[cfg(feature = "state-transition-validation")]
60use crate::consensus::ConsensusError;
61pub use traits::*;
62
63use crate::address_funds::PlatformAddress;
64use crate::data_contract::config::DataContractConfig;
65use crate::data_contract::serialized_version::DataContractInSerializationFormat;
66use crate::fee::Credits;
67#[cfg(feature = "state-transition-signing")]
68use crate::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0;
69#[cfg(feature = "state-transition-signing")]
70use crate::identity::identity_public_key::contract_bounds::BatchedTransitionBoundsCheck;
71#[cfg(feature = "state-transition-signing")]
72use crate::identity::signer::Signer;
73use crate::identity::state_transition::OptionallyAssetLockProved;
74use crate::identity::Purpose;
75#[cfg(feature = "state-transition-signing")]
76use crate::identity::{IdentityPublicKey, KeyType};
77use crate::identity::{KeyID, SecurityLevel};
78use crate::prelude::{AddressNonce, AssetLockProof, UserFeeIncrease};
79#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
80use crate::serialization::JsonConvertible;
81#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
82use crate::serialization::ValueConvertible;
83use crate::serialization::{PlatformDeserializableUntrusted, Signable};
84use crate::state_transition::address_credit_withdrawal_transition::{
85    AddressCreditWithdrawalTransition, AddressCreditWithdrawalTransitionSignable,
86};
87use crate::state_transition::address_funding_from_asset_lock_transition::{
88    AddressFundingFromAssetLockTransition, AddressFundingFromAssetLockTransitionSignable,
89};
90use crate::state_transition::address_funds_transfer_transition::{
91    AddressFundsTransferTransition, AddressFundsTransferTransitionSignable,
92};
93use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0;
94use crate::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods;
95use crate::state_transition::batch_transition::batched_transition::BatchedTransitionRef;
96#[cfg(feature = "state-transition-signing")]
97use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0;
98use crate::state_transition::batch_transition::{BatchTransition, BatchTransitionSignable};
99use crate::state_transition::contract_fee_claim_transition::{
100    ContractFeeClaimTransition, ContractFeeClaimTransitionSignable,
101};
102use crate::state_transition::contract_user_moderation_transition::{
103    ContractUserModerationTransition, ContractUserModerationTransitionSignable,
104};
105use crate::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0;
106use crate::state_transition::data_contract_create_transition::{
107    DataContractCreateTransition, DataContractCreateTransitionSignable,
108};
109use crate::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0;
110use crate::state_transition::data_contract_update_transition::{
111    DataContractUpdateTransition, DataContractUpdateTransitionSignable,
112};
113#[cfg(feature = "state-transition-signing")]
114use crate::state_transition::errors::InvalidIdentityPublicKeyTypeError;
115#[cfg(feature = "state-transition-signing")]
116use crate::state_transition::errors::InvalidSignaturePublicKeyError;
117#[cfg(all(feature = "state-transitions", feature = "validation"))]
118use crate::state_transition::errors::StateTransitionError::StateTransitionIsNotActiveError;
119#[cfg(feature = "state-transition-validation")]
120use crate::state_transition::errors::StateTransitionIsNotSignedError;
121#[cfg(feature = "state-transition-signing")]
122use crate::state_transition::errors::WrongPublicKeyPurposeError;
123use crate::state_transition::identity_create_from_addresses_transition::accessors::IdentityCreateFromAddressesTransitionAccessorsV0;
124use crate::state_transition::identity_create_from_addresses_transition::{
125    IdentityCreateFromAddressesTransition, IdentityCreateFromAddressesTransitionSignable,
126};
127use crate::state_transition::identity_create_from_shielded_pool_transition::accessors::IdentityCreateFromShieldedPoolTransitionAccessorsV0;
128use crate::state_transition::identity_create_from_shielded_pool_transition::{
129    IdentityCreateFromShieldedPoolTransition, IdentityCreateFromShieldedPoolTransitionSignable,
130};
131use crate::state_transition::identity_create_transition::accessors::IdentityCreateTransitionAccessorsV0;
132use crate::state_transition::identity_create_transition::{
133    IdentityCreateTransition, IdentityCreateTransitionSignable,
134};
135use crate::state_transition::identity_credit_transfer_to_addresses_transition::{
136    IdentityCreditTransferToAddressesTransition,
137    IdentityCreditTransferToAddressesTransitionSignable,
138};
139use crate::state_transition::identity_credit_transfer_transition::{
140    IdentityCreditTransferTransition, IdentityCreditTransferTransitionSignable,
141};
142use crate::state_transition::identity_credit_withdrawal_transition::{
143    IdentityCreditWithdrawalTransition, IdentityCreditWithdrawalTransitionSignable,
144};
145use crate::state_transition::identity_key_limits_update_transition::{
146    IdentityKeyLimitsUpdateTransition, IdentityKeyLimitsUpdateTransitionSignable,
147};
148use crate::state_transition::identity_top_up_from_shielded_pool_transition::{
149    IdentityTopUpFromShieldedPoolTransition, IdentityTopUpFromShieldedPoolTransitionSignable,
150};
151use crate::state_transition::identity_topup_from_addresses_transition::{
152    IdentityTopUpFromAddressesTransition, IdentityTopUpFromAddressesTransitionSignable,
153};
154use crate::state_transition::identity_topup_transition::{
155    IdentityTopUpTransition, IdentityTopUpTransitionSignable,
156};
157use crate::state_transition::identity_update_transition::accessors::IdentityUpdateTransitionAccessorsV0;
158use crate::state_transition::identity_update_transition::{
159    IdentityUpdateTransition, IdentityUpdateTransitionSignable,
160};
161use crate::state_transition::masternode_vote_transition::MasternodeVoteTransition;
162use crate::state_transition::masternode_vote_transition::MasternodeVoteTransitionSignable;
163use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation;
164use crate::state_transition::shield_from_asset_lock_transition::{
165    ShieldFromAssetLockTransition, ShieldFromAssetLockTransitionSignable,
166};
167use crate::state_transition::shield_from_identity_transition::{
168    ShieldFromIdentityTransition, ShieldFromIdentityTransitionSignable,
169};
170use crate::state_transition::shield_transition::{ShieldTransition, ShieldTransitionSignable};
171use crate::state_transition::shielded_transfer_transition::{
172    ShieldedTransferTransition, ShieldedTransferTransitionSignable,
173};
174use crate::state_transition::shielded_withdrawal_transition::{
175    ShieldedWithdrawalTransition, ShieldedWithdrawalTransitionSignable,
176};
177#[cfg(feature = "state-transition-signing")]
178use crate::state_transition::state_transitions::document::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0;
179use crate::state_transition::token_purchase_from_shielded_pool_transition::{
180    TokenPurchaseFromShieldedPoolTransition, TokenPurchaseFromShieldedPoolTransitionSignable,
181};
182use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::{
183    TokenShieldedTransferWithShieldedFeeTransition,
184    TokenShieldedTransferWithShieldedFeeTransitionSignable,
185};
186use crate::state_transition::token_unshield_with_shielded_fee_transition::{
187    TokenUnshieldWithShieldedFeeTransition, TokenUnshieldWithShieldedFeeTransitionSignable,
188};
189use crate::state_transition::unshield_transition::{
190    UnshieldTransition, UnshieldTransitionSignable,
191};
192use state_transitions::document::batch_transition::batched_transition::token_transition::TokenTransition;
193pub use state_transitions::*;
194
195pub type GetDataContractSecurityLevelRequirementFn =
196    fn(Identifier, String) -> Result<SecurityLevel, ProtocolError>;
197
198macro_rules! call_method {
199    ($state_transition:expr, $method:ident, $args:tt ) => {
200        match $state_transition {
201            StateTransition::DataContractCreate(st) => st.$method($args),
202            StateTransition::DataContractUpdate(st) => st.$method($args),
203            StateTransition::ContractUserModeration(st) => st.$method($args),
204            StateTransition::ContractFeeClaim(st) => st.$method($args),
205            StateTransition::Batch(st) => st.$method($args),
206            StateTransition::IdentityCreate(st) => st.$method($args),
207            StateTransition::IdentityTopUp(st) => st.$method($args),
208            StateTransition::IdentityCreditWithdrawal(st) => st.$method($args),
209            StateTransition::IdentityUpdate(st) => st.$method($args),
210            StateTransition::IdentityKeyLimitsUpdate(st) => st.$method($args),
211            StateTransition::IdentityCreditTransfer(st) => st.$method($args),
212            StateTransition::MasternodeVote(st) => st.$method($args),
213            StateTransition::IdentityCreditTransferToAddresses(st) => st.$method($args),
214            StateTransition::IdentityCreateFromAddresses(st) => st.$method($args),
215            StateTransition::IdentityTopUpFromAddresses(st) => st.$method($args),
216            StateTransition::AddressFundsTransfer(st) => st.$method($args),
217            StateTransition::AddressFundingFromAssetLock(st) => st.$method($args),
218            StateTransition::AddressCreditWithdrawal(st) => st.$method($args),
219            StateTransition::Shield(st) => st.$method($args),
220            StateTransition::ShieldedTransfer(st) => st.$method($args),
221            StateTransition::Unshield(st) => st.$method($args),
222            StateTransition::IdentityTopUpFromShieldedPool(st) => st.$method($args),
223            StateTransition::TokenShieldedTransferWithShieldedFee(st) => st.$method($args),
224            StateTransition::TokenUnshieldWithShieldedFee(st) => st.$method($args),
225            StateTransition::TokenPurchaseFromShieldedPool(st) => st.$method($args),
226            StateTransition::ShieldFromAssetLock(st) => st.$method($args),
227            StateTransition::ShieldedWithdrawal(st) => st.$method($args),
228            StateTransition::IdentityCreateFromShieldedPool(st) => st.$method($args),
229            StateTransition::ShieldFromIdentity(st) => st.$method($args),
230        }
231    };
232    ($state_transition:expr, $method:ident ) => {
233        match $state_transition {
234            StateTransition::DataContractCreate(st) => st.$method(),
235            StateTransition::DataContractUpdate(st) => st.$method(),
236            StateTransition::ContractUserModeration(st) => st.$method(),
237            StateTransition::ContractFeeClaim(st) => st.$method(),
238            StateTransition::Batch(st) => st.$method(),
239            StateTransition::IdentityCreate(st) => st.$method(),
240            StateTransition::IdentityTopUp(st) => st.$method(),
241            StateTransition::IdentityCreditWithdrawal(st) => st.$method(),
242            StateTransition::IdentityUpdate(st) => st.$method(),
243            StateTransition::IdentityKeyLimitsUpdate(st) => st.$method(),
244            StateTransition::IdentityCreditTransfer(st) => st.$method(),
245            StateTransition::MasternodeVote(st) => st.$method(),
246            StateTransition::IdentityCreditTransferToAddresses(st) => st.$method(),
247            StateTransition::IdentityCreateFromAddresses(st) => st.$method(),
248            StateTransition::IdentityTopUpFromAddresses(st) => st.$method(),
249            StateTransition::AddressFundsTransfer(st) => st.$method(),
250            StateTransition::AddressFundingFromAssetLock(st) => st.$method(),
251            StateTransition::AddressCreditWithdrawal(st) => st.$method(),
252            StateTransition::Shield(st) => st.$method(),
253            StateTransition::ShieldedTransfer(st) => st.$method(),
254            StateTransition::Unshield(st) => st.$method(),
255            StateTransition::IdentityTopUpFromShieldedPool(st) => st.$method(),
256            StateTransition::TokenShieldedTransferWithShieldedFee(st) => st.$method(),
257            StateTransition::TokenUnshieldWithShieldedFee(st) => st.$method(),
258            StateTransition::TokenPurchaseFromShieldedPool(st) => st.$method(),
259            StateTransition::ShieldFromAssetLock(st) => st.$method(),
260            StateTransition::ShieldedWithdrawal(st) => st.$method(),
261            StateTransition::IdentityCreateFromShieldedPool(st) => st.$method(),
262            StateTransition::ShieldFromIdentity(st) => st.$method(),
263        }
264    };
265}
266
267macro_rules! call_getter_method_identity_signed {
268    ($state_transition:expr, $method:ident, $args:tt ) => {
269        match $state_transition {
270            StateTransition::DataContractCreate(st) => Some(st.$method($args)),
271            StateTransition::DataContractUpdate(st) => Some(st.$method($args)),
272            StateTransition::ContractUserModeration(st) => Some(st.$method($args)),
273            StateTransition::ContractFeeClaim(st) => Some(st.$method($args)),
274            StateTransition::Batch(st) => Some(st.$method($args)),
275            StateTransition::IdentityCreate(_) => None,
276            StateTransition::IdentityTopUp(_) => None,
277            StateTransition::IdentityCreditWithdrawal(st) => Some(st.$method($args)),
278            StateTransition::IdentityUpdate(st) => Some(st.$method($args)),
279            StateTransition::IdentityKeyLimitsUpdate(st) => Some(st.$method($args)),
280            StateTransition::IdentityCreditTransfer(st) => Some(st.$method($args)),
281            StateTransition::MasternodeVote(st) => Some(st.$method($args)),
282            StateTransition::IdentityCreditTransferToAddresses(st) => Some(st.$method($args)),
283            StateTransition::IdentityCreateFromAddresses(_) => None,
284            StateTransition::IdentityTopUpFromAddresses(_) => None,
285            StateTransition::AddressFundsTransfer(_) => None,
286            StateTransition::AddressFundingFromAssetLock(_) => None,
287            StateTransition::AddressCreditWithdrawal(_) => None,
288            StateTransition::Shield(_) => None,
289            StateTransition::ShieldedTransfer(_) => None,
290            StateTransition::Unshield(_) => None,
291            StateTransition::IdentityTopUpFromShieldedPool(_) => None,
292            StateTransition::TokenShieldedTransferWithShieldedFee(_) => None,
293            StateTransition::TokenUnshieldWithShieldedFee(_) => None,
294            StateTransition::TokenPurchaseFromShieldedPool(_) => None,
295            StateTransition::ShieldFromAssetLock(_) => None,
296            StateTransition::ShieldedWithdrawal(_) => None,
297            StateTransition::IdentityCreateFromShieldedPool(_) => None,
298            StateTransition::ShieldFromIdentity(st) => Some(st.$method($args)),
299        }
300    };
301    ($state_transition:expr, $method:ident ) => {
302        match $state_transition {
303            StateTransition::DataContractCreate(st) => Some(st.$method()),
304            StateTransition::DataContractUpdate(st) => Some(st.$method()),
305            StateTransition::ContractUserModeration(st) => Some(st.$method()),
306            StateTransition::ContractFeeClaim(st) => Some(st.$method()),
307            StateTransition::Batch(st) => Some(st.$method()),
308            StateTransition::IdentityCreate(_) => None,
309            StateTransition::IdentityTopUp(_) => None,
310            StateTransition::IdentityCreditWithdrawal(st) => Some(st.$method()),
311            StateTransition::IdentityUpdate(st) => Some(st.$method()),
312            StateTransition::IdentityKeyLimitsUpdate(st) => Some(st.$method()),
313            StateTransition::IdentityCreditTransfer(st) => Some(st.$method()),
314            StateTransition::MasternodeVote(st) => Some(st.$method()),
315            StateTransition::IdentityCreditTransferToAddresses(st) => Some(st.$method()),
316            StateTransition::IdentityCreateFromAddresses(_) => None,
317            StateTransition::IdentityTopUpFromAddresses(_) => None,
318            StateTransition::AddressFundsTransfer(_) => None,
319            StateTransition::AddressFundingFromAssetLock(_) => None,
320            StateTransition::AddressCreditWithdrawal(_) => None,
321            StateTransition::Shield(_) => None,
322            StateTransition::ShieldedTransfer(_) => None,
323            StateTransition::Unshield(_) => None,
324            StateTransition::IdentityTopUpFromShieldedPool(_) => None,
325            StateTransition::TokenShieldedTransferWithShieldedFee(_) => None,
326            StateTransition::TokenUnshieldWithShieldedFee(_) => None,
327            StateTransition::TokenPurchaseFromShieldedPool(_) => None,
328            StateTransition::ShieldFromAssetLock(_) => None,
329            StateTransition::ShieldedWithdrawal(_) => None,
330            StateTransition::IdentityCreateFromShieldedPool(_) => None,
331            StateTransition::ShieldFromIdentity(st) => Some(st.$method()),
332        }
333    };
334}
335
336macro_rules! call_method_identity_signed {
337    ($state_transition:expr, $method:ident, $args:tt ) => {
338        match $state_transition {
339            StateTransition::DataContractCreate(st) => st.$method($args),
340            StateTransition::DataContractUpdate(st) => st.$method($args),
341            StateTransition::ContractUserModeration(st) => st.$method($args),
342            StateTransition::ContractFeeClaim(st) => st.$method($args),
343            StateTransition::Batch(st) => st.$method($args),
344            StateTransition::IdentityCreate(_st) => {}
345            StateTransition::IdentityTopUp(_st) => {}
346            StateTransition::IdentityCreditWithdrawal(st) => st.$method($args),
347            StateTransition::IdentityUpdate(st) => st.$method($args),
348            StateTransition::IdentityKeyLimitsUpdate(st) => st.$method($args),
349            StateTransition::IdentityCreditTransfer(st) => st.$method($args),
350            StateTransition::MasternodeVote(st) => st.$method($args),
351            StateTransition::IdentityCreditTransferToAddresses(st) => st.$method($args),
352            StateTransition::IdentityCreateFromAddresses(_) => {}
353            StateTransition::IdentityTopUpFromAddresses(_) => {}
354            StateTransition::AddressFundsTransfer(_) => {}
355            StateTransition::AddressFundingFromAssetLock(_) => {}
356            StateTransition::AddressCreditWithdrawal(_) => {}
357            StateTransition::Shield(_) => {}
358            StateTransition::ShieldedTransfer(_) => {}
359            StateTransition::Unshield(_) => {}
360            StateTransition::IdentityTopUpFromShieldedPool(_) => {}
361            StateTransition::TokenShieldedTransferWithShieldedFee(_) => {}
362            StateTransition::TokenUnshieldWithShieldedFee(_) => {}
363            StateTransition::TokenPurchaseFromShieldedPool(_) => {}
364            StateTransition::ShieldFromAssetLock(_) => {}
365            StateTransition::ShieldedWithdrawal(_) => {}
366            StateTransition::IdentityCreateFromShieldedPool(_) => {}
367            StateTransition::ShieldFromIdentity(st) => st.$method($args),
368        }
369    };
370    ($state_transition:expr, $method:ident ) => {
371        match $state_transition {
372            StateTransition::DataContractCreate(st) => st.$method(),
373            StateTransition::DataContractUpdate(st) => st.$method(),
374            StateTransition::ContractUserModeration(st) => st.$method(),
375            StateTransition::ContractFeeClaim(st) => st.$method(),
376            StateTransition::Batch(st) => st.$method(),
377            StateTransition::IdentityCreate(st) => {}
378            StateTransition::IdentityTopUp(st) => {}
379            StateTransition::IdentityCreditWithdrawal(st) => st.$method(),
380            StateTransition::IdentityUpdate(st) => st.$method(),
381            StateTransition::IdentityKeyLimitsUpdate(st) => st.$method(),
382            StateTransition::IdentityCreditTransfer(st) => st.$method(),
383            StateTransition::MasternodeVote(st) => st.$method(),
384            StateTransition::IdentityCreditTransferToAddresses(st) => st.$method(),
385            StateTransition::IdentityCreateFromAddresses(_) => {}
386            StateTransition::IdentityTopUpFromAddresses(_) => {}
387            StateTransition::AddressFundsTransfer(_) => {}
388            StateTransition::AddressFundingFromAssetLock(_) => {}
389            StateTransition::AddressCreditWithdrawal(_) => {}
390            StateTransition::Shield(_) => {}
391            StateTransition::ShieldedTransfer(_) => {}
392            StateTransition::Unshield(_) => {}
393            StateTransition::IdentityTopUpFromShieldedPool(_) => {}
394            StateTransition::TokenShieldedTransferWithShieldedFee(_) => {}
395            StateTransition::TokenUnshieldWithShieldedFee(_) => {}
396            StateTransition::TokenPurchaseFromShieldedPool(_) => {}
397            StateTransition::ShieldFromAssetLock(_) => {}
398            StateTransition::ShieldedWithdrawal(_) => {}
399            StateTransition::IdentityCreateFromShieldedPool(_) => {}
400            StateTransition::ShieldFromIdentity(st) => st.$method(),
401        }
402    };
403}
404
405#[cfg(feature = "state-transition-signing")]
406macro_rules! call_errorable_method_identity_signed {
407    ($state_transition:expr, $method:ident, $( $arg:expr ),* ) => {
408        match $state_transition {
409            StateTransition::DataContractCreate(st) => st.$method($( $arg ),*),
410            StateTransition::DataContractUpdate(st) => st.$method($( $arg ),*),
411            StateTransition::ContractUserModeration(st) => st.$method($( $arg ),*),
412            StateTransition::ContractFeeClaim(st) => st.$method($( $arg ),*),
413            StateTransition::Batch(st) => st.$method($( $arg ),*),
414            StateTransition::IdentityCreate(_) => Err(ProtocolError::CorruptedCodeExecution(
415                "identity create can not be called for identity signing".to_string(),
416            )),
417            StateTransition::IdentityTopUp(_) => Err(ProtocolError::CorruptedCodeExecution(
418                "identity top up can not be called for identity signing".to_string(),
419            )),
420            StateTransition::IdentityCreditWithdrawal(st) => st.$method($( $arg ),*),
421            StateTransition::IdentityUpdate(st) => st.$method($( $arg ),*),
422            StateTransition::IdentityKeyLimitsUpdate(st) => st.$method($( $arg ),*),
423            StateTransition::IdentityCreditTransfer(st) => st.$method($( $arg ),*),
424            StateTransition::MasternodeVote(st) => st.$method($( $arg ),*),
425            StateTransition::IdentityCreditTransferToAddresses(st) => st.$method($( $arg ),*),
426            StateTransition::IdentityCreateFromAddresses(_) => Err(ProtocolError::CorruptedCodeExecution(
427                "identity create from addresses can not be called for identity signing".to_string(),
428            )),
429            StateTransition::IdentityTopUpFromAddresses(_) => Err(ProtocolError::CorruptedCodeExecution(
430                "identity top up from addresses can not be called for identity signing".to_string(),
431            )),
432            StateTransition::AddressFundsTransfer(_) => Err(ProtocolError::CorruptedCodeExecution(
433                "address funds transfer can not be called for identity signing".to_string(),
434            )),
435            StateTransition::AddressFundingFromAssetLock(_) => Err(ProtocolError::CorruptedCodeExecution(
436                "address funding from asset lock can not be called for identity signing".to_string(),
437            )),
438            StateTransition::AddressCreditWithdrawal(_) => Err(ProtocolError::CorruptedCodeExecution(
439                "address credit withdrawal can not be called for identity signing".to_string(),
440            )),
441            StateTransition::Shield(_) => Err(ProtocolError::CorruptedCodeExecution(
442                "shield transition can not be called for identity signing".to_string(),
443            )),
444            StateTransition::ShieldedTransfer(_) => Err(ProtocolError::CorruptedCodeExecution(
445                "shielded transfer transition can not be called for identity signing".to_string(),
446            )),
447            StateTransition::Unshield(_) => Err(ProtocolError::CorruptedCodeExecution(
448                "unshield transition can not be called for identity signing".to_string(),
449            )),
450            StateTransition::IdentityTopUpFromShieldedPool(_) => Err(ProtocolError::CorruptedCodeExecution(
451                "identity top up from shielded pool transition can not be called for identity signing".to_string(),
452            )),
453            StateTransition::TokenShieldedTransferWithShieldedFee(_) => Err(ProtocolError::CorruptedCodeExecution(
454                "token shielded transfer with shielded fee transition can not be called for identity signing".to_string(),
455            )),
456            StateTransition::TokenUnshieldWithShieldedFee(_) => Err(ProtocolError::CorruptedCodeExecution(
457                "token unshield with shielded fee transition can not be called for identity signing".to_string(),
458            )),
459            StateTransition::TokenPurchaseFromShieldedPool(_) => Err(ProtocolError::CorruptedCodeExecution(
460                "token purchase from shielded pool transition can not be called for identity signing".to_string(),
461            )),
462            StateTransition::ShieldFromAssetLock(_) => Err(ProtocolError::CorruptedCodeExecution(
463                "shield from asset lock transition can not be called for identity signing".to_string(),
464            )),
465            StateTransition::ShieldedWithdrawal(_) => Err(ProtocolError::CorruptedCodeExecution(
466                "shielded withdrawal transition can not be called for identity signing".to_string(),
467            )),
468            StateTransition::IdentityCreateFromShieldedPool(_) => Err(ProtocolError::CorruptedCodeExecution(
469                "identity create from shielded pool transition can not be called for identity signing".to_string(),
470            )),
471            StateTransition::ShieldFromIdentity(st) => st.$method($( $arg ),*),
472        }
473    };
474    ($state_transition:expr, $method:ident) => {
475        match $state_transition {
476            StateTransition::DataContractCreate(st) => st.$method(),
477            StateTransition::DataContractUpdate(st) => st.$method(),
478            StateTransition::ContractUserModeration(st) => st.$method(),
479            StateTransition::ContractFeeClaim(st) => st.$method(),
480            StateTransition::Batch(st) => st.$method(),
481            StateTransition::IdentityCreate(_) => Err(ProtocolError::CorruptedCodeExecution(
482                "identity create can not be called for identity signing".to_string(),
483            )),
484            StateTransition::IdentityTopUp(_) => Err(ProtocolError::CorruptedCodeExecution(
485                "identity top up can not be called for identity signing".to_string(),
486            )),
487            StateTransition::IdentityCreditWithdrawal(st) => st.$method(),
488            StateTransition::IdentityUpdate(st) => st.$method(),
489            StateTransition::IdentityKeyLimitsUpdate(st) => st.$method(),
490            StateTransition::IdentityCreditTransfer(st) => st.$method(),
491            StateTransition::MasternodeVote(st) => st.$method(),
492            StateTransition::IdentityCreditTransferToAddresses(st) => st.$method(),
493            StateTransition::IdentityCreateFromAddresses(st) => Err(ProtocolError::CorruptedCodeExecution(
494                "identity create from addresses can not be called for identity signing".to_string(),
495            )),
496            StateTransition::IdentityTopUpFromAddresses(_) => Err(ProtocolError::CorruptedCodeExecution(
497                "identity top up from addresses can not be called for identity signing".to_string(),
498            )),
499            StateTransition::AddressFundsTransfer(_) => Err(ProtocolError::CorruptedCodeExecution(
500                "address funds transfer can not be called for identity signing".to_string(),
501            )),
502            StateTransition::AddressFundingFromAssetLock(_) => Err(ProtocolError::CorruptedCodeExecution(
503                "address funding from asset lock can not be called for identity signing".to_string(),
504            )),
505            StateTransition::AddressCreditWithdrawal(_) => Err(ProtocolError::CorruptedCodeExecution(
506                "address credit withdrawal can not be called for identity signing".to_string(),
507            )),
508            StateTransition::Shield(_) => Err(ProtocolError::CorruptedCodeExecution(
509                "shield transition can not be called for identity signing".to_string(),
510            )),
511            StateTransition::ShieldedTransfer(_) => Err(ProtocolError::CorruptedCodeExecution(
512                "shielded transfer transition can not be called for identity signing".to_string(),
513            )),
514            StateTransition::Unshield(_) => Err(ProtocolError::CorruptedCodeExecution(
515                "unshield transition can not be called for identity signing".to_string(),
516            )),
517            StateTransition::IdentityTopUpFromShieldedPool(_) => Err(ProtocolError::CorruptedCodeExecution(
518                "identity top up from shielded pool transition can not be called for identity signing".to_string(),
519            )),
520            StateTransition::TokenShieldedTransferWithShieldedFee(_) => Err(ProtocolError::CorruptedCodeExecution(
521                "token shielded transfer with shielded fee transition can not be called for identity signing".to_string(),
522            )),
523            StateTransition::TokenUnshieldWithShieldedFee(_) => Err(ProtocolError::CorruptedCodeExecution(
524                "token unshield with shielded fee transition can not be called for identity signing".to_string(),
525            )),
526            StateTransition::TokenPurchaseFromShieldedPool(_) => Err(ProtocolError::CorruptedCodeExecution(
527                "token purchase from shielded pool transition can not be called for identity signing".to_string(),
528            )),
529            StateTransition::ShieldFromAssetLock(_) => Err(ProtocolError::CorruptedCodeExecution(
530                "shield from asset lock transition can not be called for identity signing".to_string(),
531            )),
532            StateTransition::ShieldedWithdrawal(_) => Err(ProtocolError::CorruptedCodeExecution(
533                "shielded withdrawal transition can not be called for identity signing".to_string(),
534            )),
535            StateTransition::IdentityCreateFromShieldedPool(_) => Err(ProtocolError::CorruptedCodeExecution(
536                "identity create from shielded pool transition can not be called for identity signing".to_string(),
537            )),
538            StateTransition::ShieldFromIdentity(st) => st.$method(),
539        }
540    };
541}
542
543/// Byte budget of a serialized [`StateTransition`], as the `limit` of its `platform_serialize`
544/// attribute below declares it; the attribute takes a literal, so the number is repeated here.
545pub const STATE_TRANSITION_MAX_ENCODED_BYTES: usize = 100_000;
546
547#[derive(
548    Debug,
549    Clone,
550    Encode,
551    Decode,
552    PlatformSerialize,
553    PlatformDeserializeTrusted,
554    PlatformDeserializeUntrusted,
555    PlatformSignable,
556    From,
557    PartialEq,
558    DecodeUntrusted,
559)]
560// `tag = "$type"` matches the system-field convention: every serde-injected
561// discriminator key in this crate carries a `$` prefix so it never collides
562// with user-data field names. Discriminates between **semantically
563// different variants** of the same kind (rather than **versions** of one
564// logical type, which use `tag = "$formatVersion"`).
565//
566// `$type` here is at the OUTERMOST level — there's no flatten path that
567// would put it next to a base's `document_type_name` (renamed to `$type`
568// in the wire). Inner umbrellas (`DocumentTransition`, `TokenTransition`)
569// use `$action` instead because they DO flatten the document base.
570//
571// Was previously `serde(untagged)`, which made deserialize ambiguous (each
572// variant tried in order until one matched structurally). The new
573// self-describing wire shape is `{"$type": "dataContractCreate", ...inner
574// fields...}`.
575//
576// The binary wire path (`PlatformSerialize`) is unchanged — only JSON/Value
577// consumers see the new shape, and there are no rs-drive / rs-drive-abci /
578// rs-sdk callers that route the umbrella through to_json/to_object today.
579#[cfg_attr(
580    feature = "serde-conversion",
581    derive(Serialize, Deserialize),
582    serde(tag = "$type", rename_all = "camelCase")
583)]
584#[platform_serialize(unversioned)] //versioned directly, no need to use platform_version
585#[platform_serialize(limit = 100000)]
586pub enum StateTransition {
587    DataContractCreate(DataContractCreateTransition),
588    DataContractUpdate(DataContractUpdateTransition),
589    Batch(BatchTransition),
590    IdentityCreate(IdentityCreateTransition),
591    IdentityTopUp(IdentityTopUpTransition),
592    IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition),
593    IdentityUpdate(IdentityUpdateTransition),
594    IdentityCreditTransfer(IdentityCreditTransferTransition),
595    MasternodeVote(MasternodeVoteTransition),
596    IdentityCreditTransferToAddresses(IdentityCreditTransferToAddressesTransition),
597    IdentityCreateFromAddresses(IdentityCreateFromAddressesTransition),
598    IdentityTopUpFromAddresses(IdentityTopUpFromAddressesTransition),
599    AddressFundsTransfer(AddressFundsTransferTransition),
600    AddressFundingFromAssetLock(AddressFundingFromAssetLockTransition),
601    AddressCreditWithdrawal(AddressCreditWithdrawalTransition),
602    Shield(ShieldTransition),
603    ShieldedTransfer(ShieldedTransferTransition),
604    Unshield(UnshieldTransition),
605    ShieldFromAssetLock(ShieldFromAssetLockTransition),
606    ShieldedWithdrawal(ShieldedWithdrawalTransition),
607    IdentityCreateFromShieldedPool(IdentityCreateFromShieldedPoolTransition),
608    ShieldFromIdentity(ShieldFromIdentityTransition),
609    IdentityTopUpFromShieldedPool(IdentityTopUpFromShieldedPoolTransition),
610    IdentityKeyLimitsUpdate(IdentityKeyLimitsUpdateTransition),
611    ContractUserModeration(ContractUserModerationTransition),
612    ContractFeeClaim(ContractFeeClaimTransition),
613    TokenShieldedTransferWithShieldedFee(TokenShieldedTransferWithShieldedFeeTransition),
614    TokenUnshieldWithShieldedFee(TokenUnshieldWithShieldedFeeTransition),
615    TokenPurchaseFromShieldedPool(TokenPurchaseFromShieldedPoolTransition),
616}
617
618#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))]
619impl JsonConvertible for StateTransition {}
620
621#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))]
622impl ValueConvertible for StateTransition {}
623
624#[cfg(all(
625    test,
626    feature = "json-conversion",
627    feature = "value-conversion",
628    feature = "serde-conversion"
629))]
630mod json_convertible_tests {
631    use super::*;
632
633    /// Round-trip a StateTransition through both JSON and Value, asserting:
634    /// 1. The wire emits `{"$type": "<expected_tag>", ...}` (umbrella's
635    ///    `tag = "$type", rename_all = "camelCase"` is correctly applied).
636    /// 2. Round-trip preserves the variant.
637    /// 3. Round-trip preserves structural equality (PartialEq on the inner).
638    ///
639    /// Inner field shapes are covered by each inner type's dedicated
640    /// `*_with_full_wire_shape` test — this helper only exercises the
641    /// umbrella's tag-dispatch boundary. The risk it catches: an inner
642    /// variant whose serde body conflicts with the umbrella's `"$type"` key,
643    /// or a serde rename that resolves to something other than the
644    /// expected camelCase form.
645    ///
646    /// `lossy_json_int_variants`: when true, the JSON-side equality assertion
647    /// runs after `normalize_integer_variants_for_json_round_trip` on both
648    /// sides. Required for variants that embed a `DataContract` —
649    /// `document_schemas` carry sized integer variants (`U32`/`I32`) that
650    /// JSON's single Number type cannot preserve. See commit 7397c73f31.
651    fn assert_umbrella_round_trip_inner(
652        original: StateTransition,
653        expected_type_tag: &str,
654        lossy_json_int_variants: bool,
655    ) {
656        use crate::serialization::{JsonConvertible, ValueConvertible};
657
658        // JSON
659        let json = original.to_json().expect("to_json");
660        assert_eq!(
661            json["$type"], expected_type_tag,
662            "json type tag for {expected_type_tag}",
663        );
664        let recovered = StateTransition::from_json(json).expect("from_json round-trip");
665        assert_eq!(
666            std::mem::discriminant(&original),
667            std::mem::discriminant(&recovered),
668            "json round-trip variant for {expected_type_tag}",
669        );
670        if lossy_json_int_variants {
671            use crate::tests::utils::normalize_integer_variants_for_json_round_trip;
672            let mut original_canon = original.to_object().expect("to_object");
673            let mut recovered_canon = recovered.to_object().expect("to_object");
674            normalize_integer_variants_for_json_round_trip(&mut original_canon);
675            normalize_integer_variants_for_json_round_trip(&mut recovered_canon);
676            assert_eq!(
677                original_canon, recovered_canon,
678                "json round-trip equality (modulo int-variant) for {expected_type_tag}",
679            );
680        } else {
681            assert_eq!(
682                original, recovered,
683                "json round-trip equality for {expected_type_tag}"
684            );
685        }
686
687        // Value
688        let value = original.to_object().expect("to_object");
689        let map = value.as_map().expect("Value::Map");
690        let tag = map
691            .iter()
692            .find(|(k, _)| k.as_text() == Some("$type"))
693            .map(|(_, v)| v)
694            .unwrap_or_else(|| panic!("type tag missing for {expected_type_tag}"));
695        assert_eq!(
696            *tag,
697            platform_value::Value::Text(expected_type_tag.to_string()),
698            "value type tag for {expected_type_tag}",
699        );
700        let recovered = StateTransition::from_object(value).expect("from_object round-trip");
701        assert_eq!(
702            std::mem::discriminant(&original),
703            std::mem::discriminant(&recovered),
704            "value round-trip variant for {expected_type_tag}",
705        );
706        assert_eq!(
707            original, recovered,
708            "value round-trip equality for {expected_type_tag}"
709        );
710    }
711
712    fn assert_umbrella_round_trip(original: StateTransition, expected_type_tag: &str) {
713        assert_umbrella_round_trip_inner(original, expected_type_tag, false);
714    }
715
716    /// Variant of `assert_umbrella_round_trip` for transitions that embed a
717    /// `DataContract` (`DataContractCreate`, `DataContractUpdate`). JSON's
718    /// single Number type collapses sized-int variants in the embedded
719    /// `document_schemas` tree, so the JSON-side equality assertion is
720    /// run modulo integer-variant normalization. The Value path keeps its
721    /// strict bit-exact assertion (platform_value preserves sized ints).
722    fn assert_umbrella_round_trip_lossy_json_int_variants(
723        original: StateTransition,
724        expected_type_tag: &str,
725    ) {
726        assert_umbrella_round_trip_inner(original, expected_type_tag, true);
727    }
728
729    // Per-variant umbrella round-trip tests. Inner fixtures are reused from
730    // each transition's own `json_convertible_tests::fixture()` (made
731    // `pub(crate)` for this purpose) — keeps the umbrella tests in sync
732    // with the inner-type tests automatically.
733
734    #[test]
735    fn umbrella_data_contract_create() {
736        let inner = crate::state_transition::data_contract_create_transition::json_convertible_tests::fixture();
737        assert_umbrella_round_trip_lossy_json_int_variants(
738            StateTransition::DataContractCreate(inner),
739            "dataContractCreate",
740        );
741    }
742
743    #[test]
744    fn umbrella_data_contract_update() {
745        let inner = crate::state_transition::data_contract_update_transition::json_convertible_tests::fixture();
746        assert_umbrella_round_trip_lossy_json_int_variants(
747            StateTransition::DataContractUpdate(inner),
748            "dataContractUpdate",
749        );
750    }
751
752    #[test]
753    fn umbrella_batch() {
754        let inner = crate::state_transition::batch_transition::json_convertible_tests::fixture();
755        assert_umbrella_round_trip(StateTransition::Batch(inner), "batch");
756    }
757
758    #[test]
759    fn umbrella_identity_create() {
760        let inner =
761            crate::state_transition::identity_create_transition::json_convertible_tests::fixture();
762        assert_umbrella_round_trip(StateTransition::IdentityCreate(inner), "identityCreate");
763    }
764
765    #[test]
766    fn umbrella_identity_top_up() {
767        let inner =
768            crate::state_transition::identity_topup_transition::json_convertible_tests::fixture();
769        assert_umbrella_round_trip(StateTransition::IdentityTopUp(inner), "identityTopUp");
770    }
771
772    #[test]
773    fn umbrella_identity_credit_withdrawal() {
774        let inner = crate::state_transition::identity_credit_withdrawal_transition::json_convertible_tests::fixture();
775        assert_umbrella_round_trip(
776            StateTransition::IdentityCreditWithdrawal(inner),
777            "identityCreditWithdrawal",
778        );
779    }
780
781    #[test]
782    fn umbrella_identity_update() {
783        let inner =
784            crate::state_transition::identity_update_transition::json_convertible_tests::fixture();
785        assert_umbrella_round_trip(StateTransition::IdentityUpdate(inner), "identityUpdate");
786    }
787
788    #[test]
789    fn umbrella_identity_key_limits_update() {
790        let inner = crate::state_transition::identity_key_limits_update_transition::json_convertible_tests::fixture();
791        assert_umbrella_round_trip(
792            StateTransition::IdentityKeyLimitsUpdate(inner),
793            "identityKeyLimitsUpdate",
794        );
795    }
796
797    #[test]
798    fn umbrella_round_trip_contract_user_moderation() {
799        let inner = crate::state_transition::contract_user_moderation_transition::json_convertible_tests::fixture();
800        assert_umbrella_round_trip(
801            StateTransition::ContractUserModeration(inner),
802            "contractUserModeration",
803        );
804    }
805
806    #[test]
807    fn umbrella_round_trip_contract_fee_claim() {
808        let inner =
809            crate::state_transition::contract_fee_claim_transition::json_convertible_tests::fixture(
810            );
811        assert_umbrella_round_trip(StateTransition::ContractFeeClaim(inner), "contractFeeClaim");
812    }
813
814    #[test]
815    fn umbrella_identity_credit_transfer() {
816        let inner = crate::state_transition::identity_credit_transfer_transition::json_convertible_tests::fixture();
817        assert_umbrella_round_trip(
818            StateTransition::IdentityCreditTransfer(inner),
819            "identityCreditTransfer",
820        );
821    }
822
823    #[test]
824    fn umbrella_masternode_vote() {
825        let inner =
826            crate::state_transition::masternode_vote_transition::json_convertible_tests::fixture();
827        assert_umbrella_round_trip(StateTransition::MasternodeVote(inner), "masternodeVote");
828    }
829
830    #[test]
831    fn umbrella_identity_credit_transfer_to_addresses() {
832        let inner = crate::state_transition::identity_credit_transfer_to_addresses_transition::json_convertible_tests::fixture();
833        assert_umbrella_round_trip(
834            StateTransition::IdentityCreditTransferToAddresses(inner),
835            "identityCreditTransferToAddresses",
836        );
837    }
838
839    #[test]
840    fn umbrella_identity_create_from_addresses() {
841        let inner = crate::state_transition::identity_create_from_addresses_transition::json_convertible_tests::fixture();
842        assert_umbrella_round_trip(
843            StateTransition::IdentityCreateFromAddresses(inner),
844            "identityCreateFromAddresses",
845        );
846    }
847
848    #[test]
849    fn umbrella_identity_top_up_from_addresses() {
850        let inner = crate::state_transition::identity_topup_from_addresses_transition::json_convertible_tests::fixture();
851        assert_umbrella_round_trip(
852            StateTransition::IdentityTopUpFromAddresses(inner),
853            "identityTopUpFromAddresses",
854        );
855    }
856
857    #[test]
858    fn umbrella_address_funds_transfer() {
859        let inner = crate::state_transition::address_funds_transfer_transition::json_convertible_tests::fixture();
860        assert_umbrella_round_trip(
861            StateTransition::AddressFundsTransfer(inner),
862            "addressFundsTransfer",
863        );
864    }
865
866    #[test]
867    fn umbrella_address_funding_from_asset_lock() {
868        let inner = crate::state_transition::address_funding_from_asset_lock_transition::json_convertible_tests::fixture();
869        assert_umbrella_round_trip(
870            StateTransition::AddressFundingFromAssetLock(inner),
871            "addressFundingFromAssetLock",
872        );
873    }
874
875    #[test]
876    fn umbrella_address_credit_withdrawal() {
877        let inner = crate::state_transition::address_credit_withdrawal_transition::json_convertible_tests::fixture();
878        assert_umbrella_round_trip(
879            StateTransition::AddressCreditWithdrawal(inner),
880            "addressCreditWithdrawal",
881        );
882    }
883
884    #[test]
885    fn umbrella_shield() {
886        let inner = crate::state_transition::shield_transition::json_convertible_tests::fixture();
887        assert_umbrella_round_trip(StateTransition::Shield(inner), "shield");
888    }
889
890    #[test]
891    fn umbrella_shielded_transfer() {
892        let inner =
893            crate::state_transition::shielded_transfer_transition::json_convertible_tests::fixture(
894            );
895        assert_umbrella_round_trip(StateTransition::ShieldedTransfer(inner), "shieldedTransfer");
896    }
897
898    #[test]
899    fn umbrella_unshield() {
900        let inner = crate::state_transition::unshield_transition::json_convertible_tests::fixture();
901        assert_umbrella_round_trip(StateTransition::Unshield(inner), "unshield");
902    }
903
904    #[test]
905    fn umbrella_shield_from_asset_lock() {
906        let inner = crate::state_transition::shield_from_asset_lock_transition::json_convertible_tests::fixture();
907        assert_umbrella_round_trip(
908            StateTransition::ShieldFromAssetLock(inner),
909            "shieldFromAssetLock",
910        );
911    }
912
913    #[test]
914    fn umbrella_shielded_withdrawal() {
915        let inner = crate::state_transition::shielded_withdrawal_transition::json_convertible_tests::fixture();
916        assert_umbrella_round_trip(
917            StateTransition::ShieldedWithdrawal(inner),
918            "shieldedWithdrawal",
919        );
920    }
921
922    #[test]
923    fn umbrella_identity_create_from_shielded_pool() {
924        let inner = crate::state_transition::identity_create_from_shielded_pool_transition::json_convertible_tests::fixture();
925        assert_umbrella_round_trip(
926            StateTransition::IdentityCreateFromShieldedPool(inner),
927            "identityCreateFromShieldedPool",
928        );
929    }
930
931    #[test]
932    fn umbrella_identity_top_up_from_shielded_pool() {
933        let inner = crate::state_transition::identity_top_up_from_shielded_pool_transition::json_convertible_tests::fixture();
934        assert_umbrella_round_trip(
935            StateTransition::IdentityTopUpFromShieldedPool(inner),
936            "identityTopUpFromShieldedPool",
937        );
938    }
939    #[test]
940    fn umbrella_token_shielded_transfer_with_shielded_fee() {
941        let inner = crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::json_convertible_tests::fixture();
942        assert_umbrella_round_trip(
943            StateTransition::TokenShieldedTransferWithShieldedFee(inner),
944            "tokenShieldedTransferWithShieldedFee",
945        );
946    }
947    #[test]
948    fn umbrella_token_unshield_with_shielded_fee() {
949        let inner = crate::state_transition::token_unshield_with_shielded_fee_transition::json_convertible_tests::fixture();
950        assert_umbrella_round_trip(
951            StateTransition::TokenUnshieldWithShieldedFee(inner),
952            "tokenUnshieldWithShieldedFee",
953        );
954    }
955    #[test]
956    fn umbrella_token_purchase_from_shielded_pool() {
957        let inner = crate::state_transition::token_purchase_from_shielded_pool_transition::json_convertible_tests::fixture();
958        assert_umbrella_round_trip(
959            StateTransition::TokenPurchaseFromShieldedPool(inner),
960            "tokenPurchaseFromShieldedPool",
961        );
962    }
963
964    #[test]
965    fn umbrella_shield_from_identity() {
966        let inner = crate::state_transition::shield_from_identity_transition::json_convertible_tests::fixture();
967        assert_umbrella_round_trip(
968            StateTransition::ShieldFromIdentity(inner),
969            "shieldFromIdentity",
970        );
971    }
972
973    /// Every kind decodes from its untagged bytes (the inner transition serialized on its own)
974    /// into the same transition, and from its tagged bytes exactly.
975    #[test]
976    fn every_kind_decodes_untagged_and_exactly() {
977        use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable};
978
979        let transitions = [
980            StateTransition::DataContractCreate(crate::state_transition::data_contract_create_transition::json_convertible_tests::fixture()),
981            StateTransition::DataContractUpdate(crate::state_transition::data_contract_update_transition::json_convertible_tests::fixture()),
982            StateTransition::Batch(crate::state_transition::batch_transition::json_convertible_tests::fixture()),
983            StateTransition::IdentityCreate(crate::state_transition::identity_create_transition::json_convertible_tests::fixture()),
984            StateTransition::IdentityTopUp(crate::state_transition::identity_topup_transition::json_convertible_tests::fixture()),
985            StateTransition::IdentityCreditWithdrawal(crate::state_transition::identity_credit_withdrawal_transition::json_convertible_tests::fixture()),
986            StateTransition::IdentityUpdate(crate::state_transition::identity_update_transition::json_convertible_tests::fixture()),
987            StateTransition::IdentityKeyLimitsUpdate(crate::state_transition::identity_key_limits_update_transition::json_convertible_tests::fixture()),
988            StateTransition::ContractUserModeration(crate::state_transition::contract_user_moderation_transition::json_convertible_tests::fixture()),
989            StateTransition::ContractFeeClaim(crate::state_transition::contract_fee_claim_transition::json_convertible_tests::fixture()),
990            StateTransition::IdentityCreditTransfer(crate::state_transition::identity_credit_transfer_transition::json_convertible_tests::fixture()),
991            StateTransition::MasternodeVote(crate::state_transition::masternode_vote_transition::json_convertible_tests::fixture()),
992            StateTransition::IdentityCreditTransferToAddresses(crate::state_transition::identity_credit_transfer_to_addresses_transition::json_convertible_tests::fixture()),
993            StateTransition::IdentityCreateFromAddresses(crate::state_transition::identity_create_from_addresses_transition::json_convertible_tests::fixture()),
994            StateTransition::IdentityTopUpFromAddresses(crate::state_transition::identity_topup_from_addresses_transition::json_convertible_tests::fixture()),
995            StateTransition::AddressFundsTransfer(crate::state_transition::address_funds_transfer_transition::json_convertible_tests::fixture()),
996            StateTransition::AddressFundingFromAssetLock(crate::state_transition::address_funding_from_asset_lock_transition::json_convertible_tests::fixture()),
997            StateTransition::AddressCreditWithdrawal(crate::state_transition::address_credit_withdrawal_transition::json_convertible_tests::fixture()),
998            StateTransition::Shield(crate::state_transition::shield_transition::json_convertible_tests::fixture()),
999            StateTransition::ShieldedTransfer(crate::state_transition::shielded_transfer_transition::json_convertible_tests::fixture()),
1000            StateTransition::Unshield(crate::state_transition::unshield_transition::json_convertible_tests::fixture()),
1001            StateTransition::ShieldFromAssetLock(crate::state_transition::shield_from_asset_lock_transition::json_convertible_tests::fixture()),
1002            StateTransition::ShieldedWithdrawal(crate::state_transition::shielded_withdrawal_transition::json_convertible_tests::fixture()),
1003            StateTransition::IdentityCreateFromShieldedPool(crate::state_transition::identity_create_from_shielded_pool_transition::json_convertible_tests::fixture()),
1004            StateTransition::IdentityTopUpFromShieldedPool(crate::state_transition::identity_top_up_from_shielded_pool_transition::json_convertible_tests::fixture()),
1005            StateTransition::ShieldFromIdentity(crate::state_transition::shield_from_identity_transition::json_convertible_tests::fixture()),
1006        ];
1007        for transition in transitions {
1008            let tagged = transition.serialize_to_bytes().expect("serializes");
1009            assert_eq!(
1010                StateTransition::deserialize_from_bytes_untrusted_exact(&tagged).expect("tagged"),
1011                transition
1012            );
1013            // The variant tags are all below 251, so bincode's varint writes them in one byte.
1014            let untagged = &tagged[1..];
1015            assert_eq!(
1016                StateTransition::deserialize_untagged_untrusted_exact(
1017                    transition.state_transition_type(),
1018                    untagged
1019                )
1020                .unwrap_or_else(|e| panic!("{} untagged: {e}", transition.name())),
1021                transition
1022            );
1023        }
1024    }
1025}
1026
1027impl OptionallyAssetLockProved for StateTransition {
1028    fn optional_asset_lock_proof(&self) -> Option<&AssetLockProof> {
1029        match self {
1030            StateTransition::IdentityCreate(st) => st.optional_asset_lock_proof(),
1031            StateTransition::IdentityTopUp(st) => st.optional_asset_lock_proof(),
1032            StateTransition::ShieldFromAssetLock(st) => st.optional_asset_lock_proof(),
1033            _ => None,
1034        }
1035    }
1036}
1037
1038/// The state transition signing options
1039#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
1040pub struct StateTransitionSigningOptions {
1041    /// This will allow signing with any security level for debugging purposes
1042    pub allow_signing_with_any_security_level: bool,
1043    /// This will allow signing with any purpose for debugging purposes
1044    pub allow_signing_with_any_purpose: bool,
1045}
1046
1047/// The active range of a transition carrying `keys`: from protocol version 14 when one of them
1048/// is bound to a contract group or is a version 1 key (the format that can carry a budget or an
1049/// expiry), `otherwise` when none is.
1050fn active_version_range_for_keys_in_creation(
1051    keys: &[IdentityPublicKeyInCreation],
1052    otherwise: RangeInclusive<ProtocolVersion>,
1053) -> RangeInclusive<ProtocolVersion> {
1054    if IdentityPublicKeyInCreation::first_bound_to_a_contract_group(keys).is_some()
1055        || IdentityPublicKeyInCreation::first_in_version_1_format(keys).is_some()
1056    {
1057        14..=LATEST_VERSION
1058    } else {
1059        otherwise
1060    }
1061}
1062
1063/// The active range of a transition carrying `contract`: from protocol version 14 when its
1064/// config is version 2 (the format that can declare moderation), otherwise whatever the
1065/// contract's own format admits. Binaries from before protocol version 14 cannot decode a
1066/// version 2 config, so an earlier version must reject the transition without charging as they
1067/// do; admitting it would also store a moderated contract whose list trees the earlier storage
1068/// writer never creates.
1069fn active_version_range_for_contract(
1070    contract: &DataContractInSerializationFormat,
1071) -> RangeInclusive<ProtocolVersion> {
1072    if matches!(contract.config(), DataContractConfig::V2(_)) {
1073        return 14..=LATEST_VERSION;
1074    }
1075    match contract {
1076        DataContractInSerializationFormat::V0(_) => ALL_VERSIONS,
1077        DataContractInSerializationFormat::V1(_) => 9..=LATEST_VERSION,
1078    }
1079}
1080
1081/// Whether a document transition of `batch_transition` has a base of version 2 or later, the
1082/// format that can carry an action fee agreement.
1083fn batch_carries_a_version_2_document_base(batch_transition: &BatchTransition) -> bool {
1084    batch_transition
1085        .transitions_iter()
1086        .any(|transition| match transition {
1087            BatchedTransitionRef::Document(document_transition) => {
1088                document_transition.base().feature_version() >= 2
1089            }
1090            BatchedTransitionRef::Token(_) => false,
1091        })
1092}
1093
1094impl StateTransition {
1095    /// Decodes a transition from bytes that arrived from outside this node and refuses one
1096    /// whose version is not active at `platform_version`. Bytes left over after the transition
1097    /// are ignored; [`Self::deserialize_from_bytes_untrusted_exact_in_version`] refuses them.
1098    pub fn deserialize_from_bytes_untrusted_in_version(
1099        bytes: &[u8],
1100        platform_version: &PlatformVersion,
1101    ) -> Result<Self, ProtocolError> {
1102        Self::decode_untrusted_in_version(
1103            bytes,
1104            StateTransition::deserialize_from_bytes_untrusted,
1105            platform_version,
1106        )
1107    }
1108
1109    /// [`Self::deserialize_from_bytes_untrusted_in_version`] that also refuses bytes left over
1110    /// after the transition, as a `PlatformDeserializationError`. Leftover bytes are checked
1111    /// before the transition's active version range.
1112    pub fn deserialize_from_bytes_untrusted_exact_in_version(
1113        bytes: &[u8],
1114        platform_version: &PlatformVersion,
1115    ) -> Result<Self, ProtocolError> {
1116        Self::decode_untrusted_in_version(
1117            bytes,
1118            StateTransition::deserialize_from_bytes_untrusted_exact,
1119            platform_version,
1120        )
1121    }
1122
1123    #[allow(unused_variables)]
1124    fn decode_untrusted_in_version(
1125        bytes: &[u8],
1126        decode: fn(&[u8]) -> Result<Self, ProtocolError>,
1127        platform_version: &PlatformVersion,
1128    ) -> Result<Self, ProtocolError> {
1129        let max_value_depth = platform_version
1130            .system_limits
1131            .max_document_value_depth
1132            .map(usize::from);
1133        let state_transition =
1134            platform_value::with_value_decode_depth_limit(max_value_depth, || decode(bytes))?;
1135        #[cfg(all(feature = "state-transitions", feature = "validation"))]
1136        {
1137            let active_version_range = state_transition.active_version_range();
1138
1139            // Tests are done with very high protocol ranges, while we could put this behind a feature,
1140            // that would probably be overkill.
1141            if active_version_range.contains(&platform_version.protocol_version)
1142                || platform_version.protocol_version > 268435456
1143            {
1144                Ok(state_transition)
1145            } else {
1146                Err(ProtocolError::StateTransitionError(
1147                    StateTransitionIsNotActiveError {
1148                        state_transition_type: state_transition.name(),
1149                        active_version_range,
1150                        current_protocol_version: platform_version.protocol_version,
1151                    },
1152                ))
1153            }
1154        }
1155        #[cfg(not(all(feature = "state-transitions", feature = "validation")))]
1156        Ok(state_transition)
1157    }
1158
1159    pub fn active_version_range(&self) -> RangeInclusive<ProtocolVersion> {
1160        match self {
1161            StateTransition::DataContractCreate(data_contract_create_transition) => {
1162                match data_contract_create_transition {
1163                    // Version 1 carries contract groups, which exist from protocol version 14.
1164                    // The embedded contract format alone would admit it at 9 and above, where
1165                    // the group data would be silently dropped.
1166                    DataContractCreateTransition::V1(_) => 14..=LATEST_VERSION,
1167                    DataContractCreateTransition::V0(_) => active_version_range_for_contract(
1168                        data_contract_create_transition.data_contract(),
1169                    ),
1170                }
1171            }
1172            StateTransition::DataContractUpdate(data_contract_update_transition) => {
1173                active_version_range_for_contract(data_contract_update_transition.data_contract())
1174            }
1175            StateTransition::Batch(batch_transition) => {
1176                // Version 2 of the document base (the action fee agreement) exists from
1177                // protocol version 14. Binaries from before it cannot decode one, so an
1178                // earlier version rejects the batch without charging, exactly as they do.
1179                if batch_carries_a_version_2_document_base(batch_transition) {
1180                    return 14..=LATEST_VERSION;
1181                }
1182                match batch_transition {
1183                    BatchTransition::V0(_) => ALL_VERSIONS,
1184                    BatchTransition::V1(_) => 9..=LATEST_VERSION,
1185                }
1186            }
1187            // A key bound to a contract group, and a version 1 key, exist from protocol version
1188            // 14, so a transition carrying one is inactive before that: an earlier version
1189            // rejects it without charging, exactly as a binary that cannot decode it does.
1190            StateTransition::IdentityCreate(st) => {
1191                active_version_range_for_keys_in_creation(st.public_keys(), ALL_VERSIONS)
1192            }
1193            StateTransition::IdentityUpdate(st) => {
1194                active_version_range_for_keys_in_creation(st.public_keys_to_add(), ALL_VERSIONS)
1195            }
1196            StateTransition::IdentityCreateFromAddresses(st) => {
1197                active_version_range_for_keys_in_creation(st.public_keys(), 11..=LATEST_VERSION)
1198            }
1199            StateTransition::IdentityCreateFromShieldedPool(st) => {
1200                active_version_range_for_keys_in_creation(st.public_keys(), 12..=LATEST_VERSION)
1201            }
1202            StateTransition::IdentityTopUp(_)
1203            | StateTransition::IdentityCreditWithdrawal(_)
1204            | StateTransition::IdentityCreditTransfer(_)
1205            | StateTransition::MasternodeVote(_) => ALL_VERSIONS,
1206            StateTransition::IdentityCreditTransferToAddresses(_)
1207            | StateTransition::IdentityTopUpFromAddresses(_)
1208            | StateTransition::AddressFundsTransfer(_)
1209            | StateTransition::AddressFundingFromAssetLock(_)
1210            | StateTransition::AddressCreditWithdrawal(_) => 11..=LATEST_VERSION,
1211            StateTransition::Shield(_)
1212            | StateTransition::ShieldedTransfer(_)
1213            | StateTransition::Unshield(_)
1214            | StateTransition::ShieldedWithdrawal(_) => 12..=LATEST_VERSION,
1215            // From protocol version 14 the bundle must bind its kind and its asset lock, which
1216            // only version 1 does. Version 0 is refused there on its version byte, before any
1217            // proof is verified: uncharged, with its asset lock left unspent, so a version 0 still
1218            // waiting when 14 activates costs its sender nothing. There is no window in which 14
1219            // accepts version 0.
1220            StateTransition::ShieldFromAssetLock(st) => match st {
1221                ShieldFromAssetLockTransition::V0(_) => 12..=13,
1222                ShieldFromAssetLockTransition::V1(_) => 14..=LATEST_VERSION,
1223            },
1224            StateTransition::ShieldFromIdentity(_)
1225            | StateTransition::IdentityTopUpFromShieldedPool(_)
1226            | StateTransition::IdentityKeyLimitsUpdate(_)
1227            | StateTransition::ContractUserModeration(_)
1228            | StateTransition::ContractFeeClaim(_)
1229            | StateTransition::TokenShieldedTransferWithShieldedFee(_)
1230            | StateTransition::TokenUnshieldWithShieldedFee(_)
1231            | StateTransition::TokenPurchaseFromShieldedPool(_) => 14..=LATEST_VERSION,
1232        }
1233    }
1234
1235    pub fn is_identity_signed(&self) -> bool {
1236        !matches!(
1237            self,
1238            StateTransition::IdentityCreate(_)
1239                | StateTransition::IdentityTopUp(_)
1240                | StateTransition::Shield(_)
1241                | StateTransition::ShieldedTransfer(_)
1242                | StateTransition::Unshield(_)
1243                | StateTransition::ShieldFromAssetLock(_)
1244                | StateTransition::ShieldedWithdrawal(_)
1245                | StateTransition::IdentityCreateFromShieldedPool(_)
1246                | StateTransition::IdentityTopUpFromShieldedPool(_)
1247                | StateTransition::TokenShieldedTransferWithShieldedFee(_)
1248                | StateTransition::TokenUnshieldWithShieldedFee(_)
1249                | StateTransition::TokenPurchaseFromShieldedPool(_)
1250        )
1251    }
1252
1253    pub fn required_asset_lock_balance_for_processing_start(
1254        &self,
1255        platform_version: &PlatformVersion,
1256    ) -> Result<Credits, ProtocolError> {
1257        match self {
1258            StateTransition::IdentityCreate(st) => {
1259                st.calculate_min_required_fee(platform_version)
1260            }
1261            StateTransition::IdentityTopUp(st) => {
1262                st.calculate_min_required_fee(platform_version)
1263            }
1264            StateTransition::AddressFundingFromAssetLock(st) => {
1265                st.calculate_min_required_fee(platform_version)
1266            }
1267            StateTransition::ShieldFromAssetLock(st) => {
1268                st.calculate_min_required_fee(platform_version)
1269            }
1270            st => Err(ProtocolError::CorruptedCodeExecution(format!("{} is not an asset lock transaction, but we are calling required_asset_lock_balance_for_processing_start", st.name()))),
1271        }
1272    }
1273
1274    fn hash(&self, skip_signature: bool) -> Result<Vec<u8>, ProtocolError> {
1275        if skip_signature {
1276            Ok(hash_double_to_vec(self.signable_bytes()?))
1277        } else {
1278            Ok(hash_double_to_vec(
1279                crate::serialization::PlatformSerializable::serialize_to_bytes(self)?,
1280            ))
1281        }
1282    }
1283
1284    /// Returns state transition name
1285    pub fn name(&self) -> String {
1286        match self {
1287            Self::DataContractCreate(_) => "DataContractCreate".to_string(),
1288            Self::DataContractUpdate(_) => "DataContractUpdate".to_string(),
1289            Self::ContractUserModeration(_) => "ContractUserModeration".to_string(),
1290            Self::ContractFeeClaim(_) => "ContractFeeClaim".to_string(),
1291            Self::Batch(batch_transition) => {
1292                let mut document_transition_types = vec![];
1293                for transition in batch_transition.transitions_iter() {
1294                    let document_transition_name = match transition {
1295                        BatchedTransitionRef::Document(DocumentTransition::Create(_)) => "Create",
1296                        BatchedTransitionRef::Document(DocumentTransition::Replace(_)) => "Replace",
1297                        BatchedTransitionRef::Document(DocumentTransition::Delete(_)) => "Delete",
1298                        BatchedTransitionRef::Document(DocumentTransition::Transfer(_)) => {
1299                            "Transfer"
1300                        }
1301                        BatchedTransitionRef::Document(DocumentTransition::UpdatePrice(_)) => {
1302                            "UpdatePrice"
1303                        }
1304                        BatchedTransitionRef::Document(DocumentTransition::Purchase(_)) => {
1305                            "Purchase"
1306                        }
1307                        BatchedTransitionRef::Document(DocumentTransition::IndexOnlyDelete(_)) => {
1308                            "IndexOnlyDelete"
1309                        }
1310                        BatchedTransitionRef::Token(TokenTransition::Transfer(_)) => {
1311                            "TokenTransfer"
1312                        }
1313                        BatchedTransitionRef::Token(TokenTransition::Mint(_)) => "TokenMint",
1314                        BatchedTransitionRef::Token(TokenTransition::Burn(_)) => "TokenBurn",
1315                        BatchedTransitionRef::Token(TokenTransition::Freeze(_)) => "TokenFreeze",
1316                        BatchedTransitionRef::Token(TokenTransition::Unfreeze(_)) => {
1317                            "TokenUnfreeze"
1318                        }
1319                        BatchedTransitionRef::Token(TokenTransition::DestroyFrozenFunds(_)) => {
1320                            "TokenDestroyFrozenFunds"
1321                        }
1322                        BatchedTransitionRef::Token(TokenTransition::EmergencyAction(_)) => {
1323                            "TokenEmergencyAction"
1324                        }
1325                        BatchedTransitionRef::Token(TokenTransition::ConfigUpdate(_)) => {
1326                            "TokenConfigUpdate"
1327                        }
1328                        BatchedTransitionRef::Token(TokenTransition::Claim(_)) => "TokenClaim",
1329                        BatchedTransitionRef::Token(TokenTransition::DirectPurchase(_)) => {
1330                            "TokenDirectPurchase"
1331                        }
1332                        BatchedTransitionRef::Token(
1333                            TokenTransition::SetPriceForDirectPurchase(_),
1334                        ) => "SetPriceForDirectPurchase",
1335                        BatchedTransitionRef::Token(TokenTransition::Shield(_)) => "TokenShield",
1336                        BatchedTransitionRef::Token(TokenTransition::Unshield(_)) => {
1337                            "TokenUnshield"
1338                        }
1339                        BatchedTransitionRef::Token(TokenTransition::ShieldedTransfer(_)) => {
1340                            "TokenShieldedTransfer"
1341                        }
1342                        BatchedTransitionRef::Token(TokenTransition::MintToPool(_)) => {
1343                            "TokenMintToPool"
1344                        }
1345                        BatchedTransitionRef::Token(TokenTransition::BurnFromPool(_)) => {
1346                            "TokenBurnFromPool"
1347                        }
1348                        BatchedTransitionRef::Token(TokenTransition::ClaimToPool(_)) => {
1349                            "TokenClaimToPool"
1350                        }
1351                        BatchedTransitionRef::Token(TokenTransition::DirectPurchaseToPool(_)) => {
1352                            "TokenDirectPurchaseToPool"
1353                        }
1354                    };
1355                    document_transition_types.push(document_transition_name);
1356                }
1357                format!("DocumentsBatch([{}])", document_transition_types.join(", "))
1358            }
1359            Self::IdentityCreate(_) => "IdentityCreate".to_string(),
1360            Self::IdentityTopUp(_) => "IdentityTopUp".to_string(),
1361            Self::IdentityCreditWithdrawal(_) => "IdentityCreditWithdrawal".to_string(),
1362            Self::IdentityUpdate(_) => "IdentityUpdate".to_string(),
1363            Self::IdentityKeyLimitsUpdate(_) => "IdentityKeyLimitsUpdate".to_string(),
1364            Self::IdentityCreditTransfer(_) => "IdentityCreditTransfer".to_string(),
1365            Self::MasternodeVote(_) => "MasternodeVote".to_string(),
1366            Self::IdentityCreditTransferToAddresses(_) => {
1367                "IdentityCreditTransferToAddresses".to_string()
1368            }
1369            Self::IdentityCreateFromAddresses(_) => "IdentityCreateFromAddresses".to_string(),
1370            Self::IdentityTopUpFromAddresses(_) => "IdentityTopUpFromAddresses".to_string(),
1371            Self::AddressFundsTransfer(_) => "AddressFundsTransfer".to_string(),
1372            Self::AddressFundingFromAssetLock(_) => "AddressFundingFromAssetLock".to_string(),
1373            Self::AddressCreditWithdrawal(_) => "AddressCreditWithdrawal".to_string(),
1374            Self::Shield(_) => "Shield".to_string(),
1375            Self::ShieldedTransfer(_) => "ShieldedTransfer".to_string(),
1376            Self::Unshield(_) => "Unshield".to_string(),
1377            Self::IdentityTopUpFromShieldedPool(_) => "IdentityTopUpFromShieldedPool".to_string(),
1378            Self::TokenShieldedTransferWithShieldedFee(_) => {
1379                "TokenShieldedTransferWithShieldedFee".to_string()
1380            }
1381            Self::TokenUnshieldWithShieldedFee(_) => "TokenUnshieldWithShieldedFee".to_string(),
1382            Self::TokenPurchaseFromShieldedPool(_) => "TokenPurchaseFromShieldedPool".to_string(),
1383            Self::ShieldFromAssetLock(_) => "ShieldFromAssetLock".to_string(),
1384            Self::ShieldedWithdrawal(_) => "ShieldedWithdrawal".to_string(),
1385            Self::IdentityCreateFromShieldedPool(_) => "IdentityCreateFromShieldedPool".to_string(),
1386            Self::ShieldFromIdentity(_) => "ShieldFromIdentity".to_string(),
1387        }
1388    }
1389
1390    /// returns the signature as a byte-array
1391    pub fn signature(&self) -> Option<&BinaryData> {
1392        match self {
1393            StateTransition::DataContractCreate(st) => Some(st.signature()),
1394            StateTransition::DataContractUpdate(st) => Some(st.signature()),
1395            StateTransition::ContractUserModeration(st) => Some(st.signature()),
1396            StateTransition::ContractFeeClaim(st) => Some(st.signature()),
1397            StateTransition::Batch(st) => Some(st.signature()),
1398            StateTransition::IdentityCreate(st) => Some(st.signature()),
1399            StateTransition::IdentityTopUp(st) => Some(st.signature()),
1400            StateTransition::IdentityCreditWithdrawal(st) => Some(st.signature()),
1401            StateTransition::IdentityUpdate(st) => Some(st.signature()),
1402            StateTransition::IdentityKeyLimitsUpdate(st) => Some(st.signature()),
1403            StateTransition::IdentityCreditTransfer(st) => Some(st.signature()),
1404            StateTransition::MasternodeVote(st) => Some(st.signature()),
1405            StateTransition::IdentityCreditTransferToAddresses(st) => Some(st.signature()),
1406            StateTransition::IdentityCreateFromAddresses(_) => None,
1407            StateTransition::IdentityTopUpFromAddresses(_) => None,
1408            StateTransition::AddressFundsTransfer(_) => None,
1409            StateTransition::AddressFundingFromAssetLock(st) => Some(st.signature()),
1410            StateTransition::AddressCreditWithdrawal(_) => None,
1411            StateTransition::Shield(_) => None,
1412            StateTransition::ShieldedTransfer(_) => None,
1413            StateTransition::Unshield(_) => None,
1414            StateTransition::IdentityTopUpFromShieldedPool(_) => None,
1415            StateTransition::TokenShieldedTransferWithShieldedFee(_) => None,
1416            StateTransition::TokenUnshieldWithShieldedFee(_) => None,
1417            StateTransition::TokenPurchaseFromShieldedPool(_) => None,
1418            StateTransition::ShieldFromAssetLock(st) => Some(st.signature()),
1419            StateTransition::ShieldedWithdrawal(_) => None,
1420            StateTransition::IdentityCreateFromShieldedPool(_) => None,
1421            StateTransition::ShieldFromIdentity(st) => Some(st.signature()),
1422        }
1423    }
1424
1425    /// returns the number of private keys
1426    pub fn required_number_of_private_keys(&self) -> u16 {
1427        match self {
1428            StateTransition::IdentityCreateFromAddresses(st) => st.inputs().len() as u16,
1429            StateTransition::IdentityTopUpFromAddresses(st) => st.inputs().len() as u16,
1430            StateTransition::AddressFundsTransfer(st) => st.inputs().len() as u16,
1431            StateTransition::AddressCreditWithdrawal(st) => st.inputs().len() as u16,
1432            StateTransition::Shield(st) => st.inputs().len() as u16,
1433            StateTransition::ShieldedTransfer(_) => 0,
1434            StateTransition::Unshield(_) => 0,
1435            StateTransition::IdentityTopUpFromShieldedPool(_) => 0,
1436            StateTransition::TokenShieldedTransferWithShieldedFee(_) => 0,
1437            StateTransition::TokenUnshieldWithShieldedFee(_) => 0,
1438            StateTransition::TokenPurchaseFromShieldedPool(_) => 0,
1439            StateTransition::ShieldFromAssetLock(_) => 0,
1440            StateTransition::ShieldedWithdrawal(_) => 0,
1441            StateTransition::IdentityCreateFromShieldedPool(_) => 0,
1442            _ => 1,
1443        }
1444    }
1445
1446    /// returns the fee_increase additional percentage multiplier, it affects only processing costs
1447    pub fn user_fee_increase(&self) -> UserFeeIncrease {
1448        match self {
1449            StateTransition::DataContractCreate(st) => st.user_fee_increase(),
1450            StateTransition::DataContractUpdate(st) => st.user_fee_increase(),
1451            StateTransition::ContractUserModeration(st) => st.user_fee_increase(),
1452            StateTransition::ContractFeeClaim(st) => st.user_fee_increase(),
1453            StateTransition::Batch(st) => st.user_fee_increase(),
1454            StateTransition::IdentityCreate(st) => st.user_fee_increase(),
1455            StateTransition::IdentityTopUp(st) => st.user_fee_increase(),
1456            StateTransition::IdentityCreditWithdrawal(st) => st.user_fee_increase(),
1457            StateTransition::IdentityUpdate(st) => st.user_fee_increase(),
1458            StateTransition::IdentityKeyLimitsUpdate(st) => st.user_fee_increase(),
1459            StateTransition::IdentityCreditTransfer(st) => st.user_fee_increase(),
1460            StateTransition::IdentityCreditTransferToAddresses(st) => st.user_fee_increase(),
1461            StateTransition::IdentityCreateFromAddresses(st) => st.user_fee_increase(),
1462            StateTransition::IdentityTopUpFromAddresses(st) => st.user_fee_increase(),
1463            StateTransition::AddressFundsTransfer(st) => st.user_fee_increase(),
1464            StateTransition::AddressFundingFromAssetLock(st) => st.user_fee_increase(),
1465            StateTransition::AddressCreditWithdrawal(st) => st.user_fee_increase(),
1466            StateTransition::Shield(st) => st.user_fee_increase(),
1467            // These transitions don't support user fee adjustment
1468            StateTransition::ShieldFromAssetLock(_) => 0,
1469            StateTransition::MasternodeVote(_) => 0,
1470            StateTransition::ShieldedTransfer(_) => 0,
1471            StateTransition::Unshield(_) => 0,
1472            StateTransition::IdentityTopUpFromShieldedPool(_) => 0,
1473            StateTransition::TokenShieldedTransferWithShieldedFee(_) => 0,
1474            StateTransition::TokenUnshieldWithShieldedFee(_) => 0,
1475            StateTransition::TokenPurchaseFromShieldedPool(_) => 0,
1476            StateTransition::ShieldedWithdrawal(_) => 0,
1477            StateTransition::IdentityCreateFromShieldedPool(_) => 0,
1478            StateTransition::ShieldFromIdentity(st) => st.user_fee_increase(),
1479        }
1480    }
1481
1482    /// Calculates the estimated minimum fee required for this state transition.
1483    ///
1484    /// The fee is calculated based on the number of inputs, outputs, and any
1485    /// transition-specific costs (e.g., key creation costs for identity creation).
1486    ///
1487    /// # Arguments
1488    ///
1489    /// * `platform_version` - The platform version containing fee configuration.
1490    ///
1491    /// # Returns
1492    ///
1493    /// The estimated fee in credits.
1494    fn calculate_estimated_fee(
1495        &self,
1496        platform_version: &PlatformVersion,
1497    ) -> Result<Credits, ProtocolError> {
1498        call_method!(self, calculate_min_required_fee, platform_version)
1499    }
1500
1501    /// The transaction id is a single hash of the data with the signature
1502    pub fn transaction_id(&self) -> Result<[u8; 32], ProtocolError> {
1503        Ok(hash_single(
1504            crate::serialization::PlatformSerializable::serialize_to_bytes(self)?,
1505        ))
1506    }
1507
1508    /// returns the signature as a byte-array
1509    pub fn signature_public_key_id(&self) -> Option<KeyID> {
1510        call_getter_method_identity_signed!(self, signature_public_key_id)
1511    }
1512
1513    /// returns the key security level requirement for the state transition
1514    pub fn security_level_requirement(&self, purpose: Purpose) -> Option<Vec<SecurityLevel>> {
1515        call_getter_method_identity_signed!(self, security_level_requirement, purpose)
1516    }
1517
1518    /// returns the key purpose requirement for the state transition
1519    pub fn purpose_requirement(&self) -> Option<Vec<Purpose>> {
1520        call_getter_method_identity_signed!(self, purpose_requirement)
1521    }
1522
1523    /// returns the signature as a byte-array
1524    pub fn owner_id(&self) -> Option<Identifier> {
1525        match self {
1526            StateTransition::DataContractCreate(st) => Some(st.owner_id()),
1527            StateTransition::DataContractUpdate(st) => Some(st.owner_id()),
1528            StateTransition::ContractUserModeration(st) => Some(st.owner_id()),
1529            StateTransition::ContractFeeClaim(st) => Some(st.owner_id()),
1530            StateTransition::Batch(st) => Some(st.owner_id()),
1531            StateTransition::IdentityCreate(st) => Some(st.owner_id()),
1532            StateTransition::IdentityTopUp(st) => Some(st.owner_id()),
1533            StateTransition::IdentityCreditWithdrawal(st) => Some(st.owner_id()),
1534            StateTransition::IdentityUpdate(st) => Some(st.owner_id()),
1535            StateTransition::IdentityKeyLimitsUpdate(st) => Some(st.owner_id()),
1536            StateTransition::IdentityCreditTransfer(st) => Some(st.owner_id()),
1537            StateTransition::MasternodeVote(st) => Some(st.owner_id()),
1538            StateTransition::IdentityCreditTransferToAddresses(st) => Some(st.owner_id()),
1539            StateTransition::IdentityCreateFromAddresses(_) => None,
1540            StateTransition::IdentityTopUpFromAddresses(_) => None,
1541            StateTransition::AddressFundsTransfer(_) => None,
1542            StateTransition::AddressFundingFromAssetLock(_) => None,
1543            StateTransition::AddressCreditWithdrawal(_) => None,
1544            StateTransition::Shield(_) => None,
1545            StateTransition::ShieldedTransfer(_) => None,
1546            StateTransition::Unshield(_) => None,
1547            StateTransition::IdentityTopUpFromShieldedPool(_) => None,
1548            StateTransition::TokenShieldedTransferWithShieldedFee(_) => None,
1549            StateTransition::TokenUnshieldWithShieldedFee(_) => None,
1550            StateTransition::TokenPurchaseFromShieldedPool(_) => None,
1551            StateTransition::ShieldFromAssetLock(_) => None,
1552            StateTransition::ShieldedWithdrawal(_) => None,
1553            StateTransition::IdentityCreateFromShieldedPool(_) => None,
1554            StateTransition::ShieldFromIdentity(st) => Some(st.owner_id()),
1555        }
1556    }
1557
1558    /// returns the signature as a byte-array
1559    pub fn inputs(&self) -> Option<&BTreeMap<PlatformAddress, (AddressNonce, Credits)>> {
1560        match self {
1561            StateTransition::DataContractCreate(_)
1562            | StateTransition::DataContractUpdate(_)
1563            | StateTransition::ContractUserModeration(_)
1564            | StateTransition::ContractFeeClaim(_)
1565            | StateTransition::Batch(_)
1566            | StateTransition::IdentityCreate(_)
1567            | StateTransition::IdentityTopUp(_)
1568            | StateTransition::IdentityCreditWithdrawal(_)
1569            | StateTransition::IdentityUpdate(_)
1570            | StateTransition::IdentityKeyLimitsUpdate(_)
1571            | StateTransition::IdentityCreditTransfer(_)
1572            | StateTransition::MasternodeVote(_)
1573            | StateTransition::IdentityCreditTransferToAddresses(_) => None,
1574            StateTransition::IdentityCreateFromAddresses(st) => Some(st.inputs()),
1575            StateTransition::IdentityTopUpFromAddresses(st) => Some(st.inputs()),
1576            StateTransition::AddressFundsTransfer(st) => Some(st.inputs()),
1577            StateTransition::AddressFundingFromAssetLock(st) => Some(st.inputs()),
1578            StateTransition::AddressCreditWithdrawal(st) => Some(st.inputs()),
1579            StateTransition::Shield(st) => Some(st.inputs()),
1580            StateTransition::ShieldedTransfer(_) => None,
1581            StateTransition::Unshield(_) => None,
1582            StateTransition::IdentityTopUpFromShieldedPool(_) => None,
1583            StateTransition::TokenShieldedTransferWithShieldedFee(_) => None,
1584            StateTransition::TokenUnshieldWithShieldedFee(_) => None,
1585            StateTransition::TokenPurchaseFromShieldedPool(_) => None,
1586            StateTransition::ShieldFromAssetLock(_) => None,
1587            StateTransition::ShieldedWithdrawal(_) => None,
1588            StateTransition::IdentityCreateFromShieldedPool(_) => None,
1589            StateTransition::ShieldFromIdentity(_) => None,
1590        }
1591    }
1592
1593    /// returns the state transition type
1594    pub fn state_transition_type(&self) -> StateTransitionType {
1595        call_method!(self, state_transition_type)
1596    }
1597
1598    /// returns the unique identifiers for the state transition
1599    pub fn unique_identifiers(&self) -> Vec<String> {
1600        call_method!(self, unique_identifiers)
1601    }
1602
1603    /// set a new signature
1604    pub fn set_signature(&mut self, signature: BinaryData) -> bool {
1605        match self {
1606            StateTransition::DataContractCreate(st) => {
1607                st.set_signature(signature);
1608                true
1609            }
1610            StateTransition::DataContractUpdate(st) => {
1611                st.set_signature(signature);
1612                true
1613            }
1614            StateTransition::ContractUserModeration(st) => {
1615                st.set_signature(signature);
1616                true
1617            }
1618            StateTransition::ContractFeeClaim(st) => {
1619                st.set_signature(signature);
1620                true
1621            }
1622            StateTransition::Batch(st) => {
1623                st.set_signature(signature);
1624                true
1625            }
1626            StateTransition::IdentityCreate(st) => {
1627                st.set_signature(signature);
1628                true
1629            }
1630            StateTransition::IdentityTopUp(st) => {
1631                st.set_signature(signature);
1632                true
1633            }
1634            StateTransition::IdentityCreditWithdrawal(st) => {
1635                st.set_signature(signature);
1636                true
1637            }
1638            StateTransition::IdentityUpdate(st) => {
1639                st.set_signature(signature);
1640                true
1641            }
1642            StateTransition::IdentityKeyLimitsUpdate(st) => {
1643                st.set_signature(signature);
1644                true
1645            }
1646            StateTransition::IdentityCreditTransfer(st) => {
1647                st.set_signature(signature);
1648                true
1649            }
1650            StateTransition::MasternodeVote(st) => {
1651                st.set_signature(signature);
1652                true
1653            }
1654            StateTransition::IdentityCreditTransferToAddresses(st) => {
1655                st.set_signature(signature);
1656                true
1657            }
1658            StateTransition::IdentityCreateFromAddresses(_)
1659            | StateTransition::IdentityTopUpFromAddresses(_)
1660            | StateTransition::AddressFundsTransfer(_)
1661            | StateTransition::Shield(_)
1662            | StateTransition::ShieldedTransfer(_)
1663            | StateTransition::Unshield(_)
1664            | StateTransition::ShieldedWithdrawal(_)
1665            | StateTransition::IdentityCreateFromShieldedPool(_)
1666            | StateTransition::TokenShieldedTransferWithShieldedFee(_)
1667            | StateTransition::TokenUnshieldWithShieldedFee(_)
1668            | StateTransition::TokenPurchaseFromShieldedPool(_)
1669            | StateTransition::IdentityTopUpFromShieldedPool(_) => false,
1670            StateTransition::ShieldFromIdentity(st) => {
1671                st.set_signature(signature);
1672                true
1673            }
1674            StateTransition::AddressFundingFromAssetLock(st) => {
1675                st.set_signature(signature);
1676                true
1677            }
1678            StateTransition::ShieldFromAssetLock(st) => {
1679                st.set_signature(signature);
1680                true
1681            }
1682            StateTransition::AddressCreditWithdrawal(_) => false,
1683        }
1684    }
1685
1686    /// set fee multiplier
1687    pub fn set_user_fee_increase(&mut self, user_fee_increase: UserFeeIncrease) {
1688        match self {
1689            StateTransition::DataContractCreate(st) => st.set_user_fee_increase(user_fee_increase),
1690            StateTransition::DataContractUpdate(st) => st.set_user_fee_increase(user_fee_increase),
1691            StateTransition::ContractUserModeration(st) => {
1692                st.set_user_fee_increase(user_fee_increase)
1693            }
1694            StateTransition::ContractFeeClaim(st) => st.set_user_fee_increase(user_fee_increase),
1695            StateTransition::Batch(st) => st.set_user_fee_increase(user_fee_increase),
1696            StateTransition::IdentityCreate(st) => st.set_user_fee_increase(user_fee_increase),
1697            StateTransition::IdentityTopUp(st) => st.set_user_fee_increase(user_fee_increase),
1698            StateTransition::IdentityCreditWithdrawal(st) => {
1699                st.set_user_fee_increase(user_fee_increase)
1700            }
1701            StateTransition::IdentityUpdate(st) => st.set_user_fee_increase(user_fee_increase),
1702            StateTransition::IdentityKeyLimitsUpdate(st) => {
1703                st.set_user_fee_increase(user_fee_increase)
1704            }
1705            StateTransition::IdentityCreditTransfer(st) => {
1706                st.set_user_fee_increase(user_fee_increase)
1707            }
1708            StateTransition::IdentityCreditTransferToAddresses(st) => {
1709                st.set_user_fee_increase(user_fee_increase)
1710            }
1711            StateTransition::IdentityCreateFromAddresses(st) => {
1712                st.set_user_fee_increase(user_fee_increase)
1713            }
1714            StateTransition::IdentityTopUpFromAddresses(st) => {
1715                st.set_user_fee_increase(user_fee_increase)
1716            }
1717            StateTransition::AddressFundsTransfer(st) => {
1718                st.set_user_fee_increase(user_fee_increase)
1719            }
1720            StateTransition::AddressFundingFromAssetLock(st) => {
1721                st.set_user_fee_increase(user_fee_increase)
1722            }
1723            StateTransition::AddressCreditWithdrawal(st) => {
1724                st.set_user_fee_increase(user_fee_increase)
1725            }
1726            StateTransition::Shield(st) => st.set_user_fee_increase(user_fee_increase),
1727            // These transitions don't support user fee adjustment — no-op
1728            StateTransition::ShieldFromAssetLock(_) => {}
1729            StateTransition::MasternodeVote(_) => {}
1730            StateTransition::ShieldedTransfer(_) => {}
1731            StateTransition::Unshield(_) => {}
1732            StateTransition::IdentityTopUpFromShieldedPool(_) => {}
1733            StateTransition::TokenShieldedTransferWithShieldedFee(_) => {}
1734            StateTransition::TokenUnshieldWithShieldedFee(_) => {}
1735            StateTransition::TokenPurchaseFromShieldedPool(_) => {}
1736            StateTransition::ShieldedWithdrawal(_) => {}
1737            StateTransition::IdentityCreateFromShieldedPool(_) => {}
1738            StateTransition::ShieldFromIdentity(st) => st.set_user_fee_increase(user_fee_increase),
1739        }
1740    }
1741
1742    /// set a new signature
1743    pub fn set_signature_public_key_id(&mut self, public_key_id: KeyID) {
1744        call_method_identity_signed!(self, set_signature_public_key_id, public_key_id)
1745    }
1746
1747    #[cfg(feature = "state-transition-signing")]
1748    pub async fn sign_external<S: Signer<IdentityPublicKey>>(
1749        &mut self,
1750        identity_public_key: &IdentityPublicKey,
1751        signer: &S,
1752        get_data_contract_security_level_requirement: Option<
1753            impl Fn(Identifier, String) -> Result<SecurityLevel, ProtocolError>,
1754        >,
1755    ) -> Result<(), ProtocolError> {
1756        self.sign_external_with_options(
1757            identity_public_key,
1758            signer,
1759            get_data_contract_security_level_requirement,
1760            StateTransitionSigningOptions::default(),
1761        )
1762        .await
1763    }
1764
1765    #[cfg(feature = "state-transition-signing")]
1766    pub async fn sign_external_with_options<S: Signer<IdentityPublicKey>>(
1767        &mut self,
1768        identity_public_key: &IdentityPublicKey,
1769        signer: &S,
1770        get_data_contract_security_level_requirement: Option<
1771            impl Fn(Identifier, String) -> Result<SecurityLevel, ProtocolError>,
1772        >,
1773        options: StateTransitionSigningOptions,
1774    ) -> Result<(), ProtocolError> {
1775        self.verify_identity_key_bounds(identity_public_key)?;
1776        match self {
1777            StateTransition::DataContractCreate(st) => {
1778                st.verify_public_key_level_and_purpose(identity_public_key, options)?;
1779                st.verify_public_key_is_enabled(identity_public_key)?;
1780            }
1781            StateTransition::DataContractUpdate(st) => {
1782                st.verify_public_key_level_and_purpose(identity_public_key, options)?;
1783                st.verify_public_key_is_enabled(identity_public_key)?;
1784            }
1785            StateTransition::ContractUserModeration(st) => {
1786                st.verify_public_key_level_and_purpose(identity_public_key, options)?;
1787                st.verify_public_key_is_enabled(identity_public_key)?;
1788            }
1789            StateTransition::ContractFeeClaim(st) => {
1790                st.verify_public_key_level_and_purpose(identity_public_key, options)?;
1791                st.verify_public_key_is_enabled(identity_public_key)?;
1792            }
1793            StateTransition::Batch(st) => {
1794                let allow_token_transfer_keys = st.transitions_len() == 1
1795                    && (st
1796                        .first_transition()
1797                        .expect("expected first transition with len 1")
1798                        .as_transition_token_claim()
1799                        .is_some()
1800                        || st
1801                            .first_transition()
1802                            .expect("expected first transition with len 1")
1803                            .as_transition_token_transfer()
1804                            .is_some());
1805                let allowed_key_purposes = if allow_token_transfer_keys {
1806                    vec![Purpose::AUTHENTICATION, Purpose::TRANSFER]
1807                } else {
1808                    vec![Purpose::AUTHENTICATION]
1809                };
1810                if !options.allow_signing_with_any_purpose
1811                    && !allowed_key_purposes.contains(&identity_public_key.purpose())
1812                {
1813                    return Err(ProtocolError::WrongPublicKeyPurposeError(
1814                        WrongPublicKeyPurposeError::new(
1815                            identity_public_key.purpose(),
1816                            allowed_key_purposes,
1817                        ),
1818                    ));
1819                }
1820                if !options.allow_signing_with_any_security_level {
1821                    let security_level_requirement = st.combined_security_level_requirement(
1822                        get_data_contract_security_level_requirement,
1823                    )?;
1824                    if !security_level_requirement.contains(&identity_public_key.security_level()) {
1825                        return Err(ProtocolError::InvalidSignaturePublicKeySecurityLevelError(
1826                            InvalidSignaturePublicKeySecurityLevelError::new(
1827                                identity_public_key.security_level(),
1828                                security_level_requirement,
1829                            ),
1830                        ));
1831                    }
1832                }
1833                st.verify_public_key_is_enabled(identity_public_key)?;
1834            }
1835            StateTransition::IdentityCreditWithdrawal(st) => {
1836                st.verify_public_key_level_and_purpose(identity_public_key, options)?;
1837                st.verify_public_key_is_enabled(identity_public_key)?;
1838            }
1839            StateTransition::IdentityUpdate(st) => {
1840                st.verify_public_key_level_and_purpose(identity_public_key, options)?;
1841                st.verify_public_key_is_enabled(identity_public_key)?;
1842            }
1843            StateTransition::IdentityKeyLimitsUpdate(st) => {
1844                st.verify_public_key_level_and_purpose(identity_public_key, options)?;
1845                st.verify_public_key_is_enabled(identity_public_key)?;
1846            }
1847            StateTransition::IdentityCreditTransfer(st) => {
1848                st.verify_public_key_level_and_purpose(identity_public_key, options)?;
1849                st.verify_public_key_is_enabled(identity_public_key)?;
1850            }
1851            StateTransition::IdentityCreate(_) => {
1852                return Err(ProtocolError::CorruptedCodeExecution(
1853                    "identity create can not be called for identity signing".to_string(),
1854                ))
1855            }
1856            StateTransition::IdentityTopUp(_) => {
1857                return Err(ProtocolError::CorruptedCodeExecution(
1858                    "identity top up can not be called for identity signing".to_string(),
1859                ))
1860            }
1861            StateTransition::MasternodeVote(st) => {
1862                st.verify_public_key_level_and_purpose(identity_public_key, options)?;
1863                st.verify_public_key_is_enabled(identity_public_key)?;
1864            }
1865            StateTransition::IdentityCreditTransferToAddresses(st) => {
1866                st.verify_public_key_level_and_purpose(identity_public_key, options)?;
1867                st.verify_public_key_is_enabled(identity_public_key)?;
1868            }
1869            StateTransition::IdentityCreateFromAddresses(_) => {
1870                return Err(ProtocolError::CorruptedCodeExecution(
1871                    "identity create from addresses can not be called for identity signing"
1872                        .to_string(),
1873                ))
1874            }
1875            StateTransition::IdentityTopUpFromAddresses(_) => {
1876                return Err(ProtocolError::CorruptedCodeExecution(
1877                    "identity top up from addresses can not be called for identity signing"
1878                        .to_string(),
1879                ))
1880            }
1881            StateTransition::AddressFundsTransfer(_) => {
1882                return Err(ProtocolError::CorruptedCodeExecution(
1883                    "address funds transfer transition can not be called for identity signing"
1884                        .to_string(),
1885                ))
1886            }
1887            StateTransition::AddressFundingFromAssetLock(_) => {
1888                return Err(ProtocolError::CorruptedCodeExecution(
1889                    "address funding from asset lock transition can not be called for identity signing"
1890                        .to_string(),
1891                ))
1892            }
1893            StateTransition::AddressCreditWithdrawal(_) => {
1894                return Err(ProtocolError::CorruptedCodeExecution(
1895                    "address credit withdrawal transition can not be called for identity signing"
1896                        .to_string(),
1897                ))
1898            }
1899            StateTransition::Shield(_) => {
1900                return Err(ProtocolError::CorruptedCodeExecution(
1901                    "shield transition can not be called for identity signing".to_string(),
1902                ))
1903            }
1904            StateTransition::ShieldedTransfer(_) => {
1905                return Err(ProtocolError::CorruptedCodeExecution(
1906                    "shielded transfer transition can not be called for identity signing"
1907                        .to_string(),
1908                ))
1909            }
1910            StateTransition::Unshield(_) => {
1911                return Err(ProtocolError::CorruptedCodeExecution(
1912                    "unshield transition can not be called for identity signing".to_string(),
1913                ))
1914            }
1915            StateTransition::IdentityTopUpFromShieldedPool(_) => {
1916                return Err(ProtocolError::CorruptedCodeExecution(
1917                    "identity top up from shielded pool transition can not be called for identity signing".to_string(),
1918                ))
1919            }
1920            StateTransition::TokenShieldedTransferWithShieldedFee(_) => {
1921                return Err(ProtocolError::CorruptedCodeExecution(
1922                    "token shielded transfer with shielded fee transition can not be called for identity signing".to_string(),
1923                ))
1924            }
1925            StateTransition::TokenUnshieldWithShieldedFee(_) => {
1926                return Err(ProtocolError::CorruptedCodeExecution(
1927                    "token unshield with shielded fee transition can not be called for identity signing".to_string(),
1928                ))
1929            }
1930            StateTransition::TokenPurchaseFromShieldedPool(_) => {
1931                return Err(ProtocolError::CorruptedCodeExecution(
1932                    "token purchase from shielded pool transition can not be called for identity signing".to_string(),
1933                ))
1934            }
1935            StateTransition::ShieldFromAssetLock(_) => {
1936                return Err(ProtocolError::CorruptedCodeExecution(
1937                    "shield from asset lock transition can not be called for identity signing"
1938                        .to_string(),
1939                ))
1940            }
1941            StateTransition::ShieldedWithdrawal(_) => {
1942                return Err(ProtocolError::CorruptedCodeExecution(
1943                    "shielded withdrawal transition can not be called for identity signing"
1944                        .to_string(),
1945                ))
1946            }
1947            StateTransition::IdentityCreateFromShieldedPool(_) => {
1948                return Err(ProtocolError::CorruptedCodeExecution(
1949                    "identity create from shielded pool transition can not be called for identity signing"
1950                        .to_string(),
1951                ))
1952            }
1953            StateTransition::ShieldFromIdentity(st) => {
1954                st.verify_public_key_level_and_purpose(identity_public_key, options)?;
1955                st.verify_public_key_is_enabled(identity_public_key)?;
1956            }
1957        }
1958        let data = self.signable_bytes()?;
1959        self.set_signature(signer.sign(identity_public_key, data.as_slice()).await?);
1960        self.set_signature_public_key_id(identity_public_key.id());
1961        Ok(())
1962    }
1963
1964    /// A contract-bound AUTHENTICATION key may only sign a Batch whose members are all inside
1965    /// its bounds. Consensus enforces the same rule from the stored key; checking here saves the
1966    /// round trip when the signing API is handed the key metadata. A contract group bound is
1967    /// left to consensus, which reads the group's memberships.
1968    #[cfg(feature = "state-transition-signing")]
1969    fn verify_identity_key_bounds(
1970        &self,
1971        identity_public_key: &IdentityPublicKey,
1972    ) -> Result<(), ProtocolError> {
1973        if identity_public_key.purpose() != Purpose::AUTHENTICATION {
1974            return Ok(());
1975        }
1976        let Some(bounds) = identity_public_key.contract_bounds() else {
1977            return Ok(());
1978        };
1979        match self {
1980            StateTransition::Batch(batch) => {
1981                use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0;
1982                if batch.transitions_iter().all(|member| {
1983                    bounds.check_batched_transition(member) != BatchedTransitionBoundsCheck::Denied
1984                }) {
1985                    Ok(())
1986                } else {
1987                    Err(ProtocolError::ConsensusError(Box::new(
1988                        ContractBoundedKeyOutOfBoundsError::new(identity_public_key.id()).into(),
1989                    )))
1990                }
1991            }
1992            _ => Err(ProtocolError::ConsensusError(Box::new(
1993                ContractBoundedKeyNonBatchError::new(identity_public_key.id()).into(),
1994            ))),
1995        }
1996    }
1997
1998    #[cfg(feature = "state-transition-signing")]
1999    pub fn sign(
2000        &mut self,
2001        identity_public_key: &IdentityPublicKey,
2002        private_key: &[u8],
2003        bls: &impl BlsModule,
2004    ) -> Result<(), ProtocolError> {
2005        self.sign_with_options(
2006            identity_public_key,
2007            private_key,
2008            bls,
2009            StateTransitionSigningOptions::default(),
2010        )
2011    }
2012
2013    #[cfg(feature = "state-transition-signing")]
2014    pub fn sign_with_options(
2015        &mut self,
2016        identity_public_key: &IdentityPublicKey,
2017        private_key: &[u8],
2018        bls: &impl BlsModule,
2019        options: StateTransitionSigningOptions,
2020    ) -> Result<(), ProtocolError> {
2021        self.verify_identity_key_bounds(identity_public_key)?;
2022        call_errorable_method_identity_signed!(
2023            self,
2024            verify_public_key_level_and_purpose,
2025            identity_public_key,
2026            options
2027        )?;
2028        call_errorable_method_identity_signed!(
2029            self,
2030            verify_public_key_is_enabled,
2031            identity_public_key
2032        )?;
2033
2034        match identity_public_key.key_type() {
2035            KeyType::ECDSA_SECP256K1 => {
2036                let public_key_compressed = get_compressed_public_ec_key(private_key)?;
2037
2038                // we store compressed public key in the identity ,
2039                // and here we compare the private key used to sing the state transition with
2040                // the compressed key stored in the identity
2041
2042                if public_key_compressed.as_slice() != identity_public_key.data().as_slice() {
2043                    return Err(ProtocolError::InvalidSignaturePublicKeyError(
2044                        InvalidSignaturePublicKeyError::new(identity_public_key.data().to_vec()),
2045                    ));
2046                }
2047
2048                self.sign_by_private_key(private_key, identity_public_key.key_type(), bls)
2049            }
2050            KeyType::ECDSA_HASH160 => {
2051                let public_key_compressed = get_compressed_public_ec_key(private_key)?;
2052                let pub_key_hash = ripemd160_sha256(&public_key_compressed);
2053
2054                if identity_public_key.data().as_slice() != pub_key_hash {
2055                    return Err(ProtocolError::InvalidSignaturePublicKeyError(
2056                        InvalidSignaturePublicKeyError::new(identity_public_key.data().to_vec()),
2057                    ));
2058                }
2059                self.sign_by_private_key(private_key, identity_public_key.key_type(), bls)
2060            }
2061            KeyType::BLS12_381 => {
2062                let public_key = bls.private_key_to_public_key(private_key)?;
2063
2064                if public_key != identity_public_key.data().as_slice() {
2065                    return Err(ProtocolError::InvalidSignaturePublicKeyError(
2066                        InvalidSignaturePublicKeyError::new(identity_public_key.data().to_vec()),
2067                    ));
2068                }
2069                self.sign_by_private_key(private_key, identity_public_key.key_type(), bls)
2070            }
2071
2072            // the default behavior from
2073            // https://github.com/dashevo/platform/blob/6b02b26e5cd3a7c877c5fdfe40c4a4385a8dda15/packages/js-dpp/lib/stateTransition/AbstractStateTransitionIdentitySigned.js#L108
2074            // is to return the error for the BIP13_SCRIPT_HASH
2075            KeyType::BIP13_SCRIPT_HASH | KeyType::EDDSA_25519_HASH160 => {
2076                Err(ProtocolError::InvalidIdentityPublicKeyTypeError(
2077                    InvalidIdentityPublicKeyTypeError::new(identity_public_key.key_type()),
2078                ))
2079            }
2080        }?;
2081
2082        self.set_signature_public_key_id(identity_public_key.id());
2083
2084        Ok(())
2085    }
2086
2087    #[cfg(feature = "state-transition-signing")]
2088    /// Signs data with the private key
2089    pub fn sign_by_private_key(
2090        &mut self,
2091        private_key: &[u8],
2092        key_type: KeyType,
2093        bls: &impl BlsModule,
2094    ) -> Result<(), ProtocolError> {
2095        let data = self.signable_bytes()?;
2096        match key_type {
2097            KeyType::BLS12_381 => {
2098                if !self.set_signature(bls.sign(&data, private_key)?.into()) {
2099                    return Err(ProtocolError::InvalidVerificationWrongNumberOfElements {
2100                        needed: self.required_number_of_private_keys(),
2101                        using: 1,
2102                        msg: "failed to set BLS signature",
2103                    });
2104                }
2105            }
2106
2107            // https://github.com/dashevo/platform/blob/9c8e6a3b6afbc330a6ab551a689de8ccd63f9120/packages/js-dpp/lib/stateTransition/AbstractStateTransition.js#L169
2108            KeyType::ECDSA_SECP256K1 | KeyType::ECDSA_HASH160 => {
2109                let signature = signer::sign(&data, private_key)?;
2110                if !self.set_signature(signature.to_vec().into()) {
2111                    return Err(ProtocolError::InvalidVerificationWrongNumberOfElements {
2112                        needed: self.required_number_of_private_keys(),
2113                        using: 1,
2114                        msg: "failed to set ECDSA signature",
2115                    });
2116                };
2117            }
2118
2119            // the default behavior from
2120            // https://github.com/dashevo/platform/blob/6b02b26e5cd3a7c877c5fdfe40c4a4385a8dda15/packages/js-dpp/lib/stateTransition/AbstractStateTransition.js#L187
2121            // is to return the error for the BIP13_SCRIPT_HASH
2122            KeyType::BIP13_SCRIPT_HASH | KeyType::EDDSA_25519_HASH160 => {
2123                return Err(ProtocolError::InvalidIdentityPublicKeyTypeError(
2124                    InvalidIdentityPublicKeyTypeError::new(key_type),
2125                ))
2126            }
2127        };
2128        Ok(())
2129    }
2130
2131    /// Sign `self.signable_bytes()` with an external Core-wallet signer and
2132    /// store the resulting Core-ECDSA signature in the transition's wrapper
2133    /// signature field.
2134    ///
2135    /// # Position in the signing-primitive family
2136    ///
2137    /// This is a **primitive** in the same family as
2138    /// [`Self::sign_by_private_key`] — it performs no validation of the
2139    /// transition variant, the key, or the relationship between them. It is
2140    /// the external-custody sibling of `sign_by_private_key`:
2141    ///
2142    /// | Primitive | Key source | Validation |
2143    /// |---|---|---|
2144    /// | [`Self::sign_by_private_key`] | raw `&[u8]` in host memory | none |
2145    /// | `sign_with_core_signer` | external signer (HSM / hardware wallet / secure enclave / remote signing service), key reached via BIP32 [`DerivationPath`] | none |
2146    ///
2147    /// Both produce **byte-identical** wrapper signatures over the same
2148    /// digest when given the same underlying private key (proven by
2149    /// `sign_with_signer_matches_sign_by_private_key_byte_for_byte` in this
2150    /// file's tests). The only difference is where the key bytes live: in
2151    /// host memory vs inside the signer's trust boundary. The signer
2152    /// performs the derive + sign + zeroise sequence atomically; this
2153    /// function never sees raw key material, only a 32-byte digest and the
2154    /// resulting signature.
2155    ///
2156    /// # Scope (what the BIP32 path means)
2157    ///
2158    /// The `path` parameter selects a key in the signer's Core wallet
2159    /// (BIP32-derived). For that path's signature to be **meaningful** the
2160    /// transition's wrapper signature field must itself carry a Core-key
2161    /// signature. Today that is exactly the four asset-lock-signed
2162    /// variants — `IdentityCreate`, `IdentityTopUp`,
2163    /// `AddressFundingFromAssetLock`, `ShieldFromAssetLock` — where the
2164    /// wrapper signature is the asset-lock proof signed by the credit
2165    /// output's Core key.
2166    ///
2167    /// For identity-signed variants (`DataContractCreate`, `Batch`,
2168    /// `IdentityCreditTransfer`, etc.) the wrapper signature is an
2169    /// identity-key signature paired with a `signature_public_key_id`,
2170    /// and the right external-signer entry point is [`Self::sign_external`]
2171    /// with a [`Signer<IdentityPublicKey>`](crate::identity::signer::Signer).
2172    /// Calling `sign_with_core_signer` on such a variant compiles and
2173    /// produces a structurally valid 65-byte signature, but the signature
2174    /// is **semantically meaningless** — Platform validation will reject
2175    /// the transition because the signature doesn't match the expected
2176    /// identity public key and `signature_public_key_id` isn't set. The
2177    /// same caveat applies to misusing `sign_by_private_key`, the sibling
2178    /// primitive — both rely on the caller passing a key the wrapper
2179    /// signature is *meant* to carry.
2180    ///
2181    /// # Wire-format parity with `sign_by_private_key`
2182    ///
2183    /// The byte layout of the stored signature mirrors
2184    /// `dashcore::signer::sign`:
2185    ///
2186    /// 1. `digest = double_sha256(self.signable_bytes()?)`
2187    /// 2. `signer.sign_ecdsa(path, digest).await` → non-recoverable
2188    ///    `(secp256k1::ecdsa::Signature, secp256k1::PublicKey)`.
2189    /// 3. Recover the recovery id by trying all four candidates against the
2190    ///    returned public key (libsecp256k1 normalises both signing paths to
2191    ///    low-s form so the 64-byte `r||s` payload is bit-identical).
2192    /// 4. Serialise as a 65-byte compact recoverable signature with the
2193    ///    `compressed` prefix convention used by `CompactSignature` — i.e.
2194    ///    `[recovery_id + 27 + 4, r (32) || s (32)]`.
2195    ///
2196    /// # Errors
2197    ///
2198    /// - Returns [`ProtocolError::ExternalSignerError`] wrapping the signer's
2199    ///   `Display` error when the underlying signer fails.
2200    /// - Returns [`ProtocolError::ExternalSignerError`] if no recovery id
2201    ///   matches the public key returned by the signer — this should be
2202    ///   unreachable for a conformant signer (invariant violation by a
2203    ///   non-conformant signer) but is surfaced rather than panicked on.
2204    /// - Returns [`ProtocolError::Generic`] if the SHA-256 transform did not
2205    ///   yield a 32-byte digest (defensive — should never happen).
2206    /// - Returns [`ProtocolError::InvalidVerificationWrongNumberOfElements`] if
2207    ///   `set_signature` rejects the result (matches `sign_by_private_key`).
2208    #[cfg(all(feature = "state-transition-signing", feature = "core_key_wallet"))]
2209    pub async fn sign_with_core_signer<S: ::key_wallet::signer::Signer>(
2210        &mut self,
2211        path: &::key_wallet::bip32::DerivationPath,
2212        signer: &S,
2213    ) -> Result<(), ProtocolError> {
2214        use dashcore::secp256k1::ecdsa::{RecoverableSignature, RecoveryId};
2215        use dashcore::secp256k1::{Message, Secp256k1};
2216        use dashcore::signer::{double_sha, CompactSignature};
2217
2218        let data = self.signable_bytes()?;
2219        // Pre-image transform matches `dashcore::signer::sign`: double-SHA256
2220        // of the signable bytes is the actual ECDSA message digest.
2221        let data_hash = double_sha(&data);
2222        let digest: [u8; 32] = data_hash.as_slice().try_into().map_err(|_| {
2223            ProtocolError::Generic("double_sha did not return 32 bytes".to_string())
2224        })?;
2225
2226        let (signature, public_key) = signer
2227            .sign_ecdsa(path, digest)
2228            .await
2229            .map_err(|e| ProtocolError::ExternalSignerError(format!("signer failed: {}", e)))?;
2230
2231        // The signer returns a non-recoverable signature. The legacy path
2232        // stores a 65-byte recoverable compact signature, so we brute-force
2233        // the recovery id (0..3) by reconstructing a `RecoverableSignature`
2234        // and comparing the recovered public key with the one the signer
2235        // returned. secp256k1 normalises both `sign_ecdsa` and
2236        // `sign_ecdsa_recoverable` outputs to low-s form, so the 64-byte
2237        // `r||s` payload is bit-identical to what `dashcore::signer::sign`
2238        // produces.
2239        let compact_64 = signature.serialize_compact();
2240        let secp = Secp256k1::new();
2241        let msg = Message::from_digest(digest);
2242
2243        let mut found: Option<RecoverableSignature> = None;
2244        for id in 0..4i32 {
2245            let recid = match RecoveryId::try_from(id) {
2246                Ok(r) => r,
2247                Err(_) => continue,
2248            };
2249            let candidate = match RecoverableSignature::from_compact(&compact_64, recid) {
2250                Ok(s) => s,
2251                Err(_) => continue,
2252            };
2253            if let Ok(recovered) = secp.recover_ecdsa(&msg, &candidate) {
2254                if recovered == public_key {
2255                    found = Some(candidate);
2256                    break;
2257                }
2258            }
2259        }
2260        let recoverable = found.ok_or_else(|| {
2261            // Invariant violation by a non-conformant signer: the
2262            // signature returned does not correspond to the public
2263            // key the signer claims. Surface as ExternalSignerError
2264            // (NOT Generic) so callers can distinguish signer-side
2265            // failures from protocol-level invariants.
2266            ProtocolError::ExternalSignerError(
2267                "signer returned a signature whose recovery id does not match the returned public key".to_string(),
2268            )
2269        })?;
2270
2271        // Compressed-pubkey convention matches `dashcore::signer::sign`, which
2272        // always passes `true` regardless of the underlying key encoding. The
2273        // signer's `sign_ecdsa` returns the compressed `secp256k1::PublicKey`,
2274        // so this is consistent.
2275        let compact_65 = recoverable.to_compact_signature(true);
2276
2277        if !self.set_signature(compact_65.to_vec().into()) {
2278            return Err(ProtocolError::InvalidVerificationWrongNumberOfElements {
2279                needed: self.required_number_of_private_keys(),
2280                using: 1,
2281                msg: "failed to set ECDSA signature",
2282            });
2283        }
2284        Ok(())
2285    }
2286
2287    #[cfg(feature = "state-transition-validation")]
2288    fn verify_ecdsa_hash_160_signature_by_public_key_hash(
2289        &self,
2290        public_key_hash: &[u8],
2291    ) -> Result<(), ProtocolError> {
2292        let Some(signature) = self.signature() else {
2293            return Err(ProtocolError::InvalidVerificationWrongNumberOfElements {
2294                needed: self.required_number_of_private_keys(),
2295                using: 1,
2296                msg: "This state transition type should a single signature",
2297            });
2298        };
2299        if signature.is_empty() {
2300            return Err(ProtocolError::StateTransitionIsNotSignedError(
2301                StateTransitionIsNotSignedError::new(self.clone()),
2302            ));
2303        }
2304        let data = self.signable_bytes()?;
2305        let data_hash = double_sha(data);
2306        signer::verify_hash_signature(&data_hash, signature.as_slice(), public_key_hash).map_err(
2307            |e| {
2308                ProtocolError::from(ConsensusError::SignatureError(
2309                    SignatureError::InvalidStateTransitionSignatureError(
2310                        InvalidStateTransitionSignatureError::new(e.to_string()),
2311                    ),
2312                ))
2313            },
2314        )
2315    }
2316
2317    #[cfg(feature = "state-transition-validation")]
2318    /// Verifies an ECDSA signature with the public key
2319    fn verify_ecdsa_signature_by_public_key(&self, public_key: &[u8]) -> Result<(), ProtocolError> {
2320        let Some(signature) = self.signature() else {
2321            return Err(ProtocolError::InvalidVerificationWrongNumberOfElements {
2322                needed: self.required_number_of_private_keys(),
2323                using: 1,
2324                msg: "This state transition type should a single signature",
2325            });
2326        };
2327        if signature.is_empty() {
2328            return Err(ProtocolError::StateTransitionIsNotSignedError(
2329                StateTransitionIsNotSignedError::new(self.clone()),
2330            ));
2331        }
2332        let data = self.signable_bytes()?;
2333        signer::verify_data_signature(&data, signature.as_slice(), public_key).map_err(|e| {
2334            // TODO: it shouldn't respond with consensus error
2335
2336            ProtocolError::from(ConsensusError::SignatureError(
2337                SignatureError::InvalidStateTransitionSignatureError(
2338                    InvalidStateTransitionSignatureError::new(e.to_string()),
2339                ),
2340            ))
2341        })
2342    }
2343
2344    #[cfg(feature = "state-transition-validation")]
2345    /// Verifies a BLS signature with the public key: `Ok(false)` when the signature is well
2346    /// formed but does not verify, an error when the key or the signature cannot be read
2347    fn verify_bls_signature_by_public_key<T: BlsModule>(
2348        &self,
2349        public_key: &[u8],
2350        bls: &T,
2351    ) -> Result<bool, ProtocolError> {
2352        let Some(signature) = self.signature() else {
2353            return Err(ProtocolError::InvalidVerificationWrongNumberOfElements {
2354                needed: self.required_number_of_private_keys(),
2355                using: 1,
2356                msg: "This state transition type should a single signature",
2357            });
2358        };
2359        if signature.is_empty() {
2360            return Err(ProtocolError::StateTransitionIsNotSignedError(
2361                StateTransitionIsNotSignedError::new(self.clone()),
2362            ));
2363        }
2364
2365        let data = self.signable_bytes()?;
2366
2367        bls.verify_signature(signature.as_slice(), &data, public_key)
2368            .map_err(|e| {
2369                // TODO: it shouldn't respond with consensus error
2370                ProtocolError::from(ConsensusError::SignatureError(
2371                    SignatureError::InvalidStateTransitionSignatureError(
2372                        InvalidStateTransitionSignatureError::new(e.to_string()),
2373                    ),
2374                ))
2375            })
2376    }
2377}
2378
2379#[cfg(feature = "state-transition-validation")]
2380impl StateTransitionStructureValidation for StateTransition {
2381    fn validate_structure(
2382        &self,
2383        platform_version: &PlatformVersion,
2384    ) -> crate::validation::SimpleConsensusValidationResult {
2385        match self {
2386            StateTransition::DataContractCreate(_)
2387            | StateTransition::DataContractUpdate(_)
2388            | StateTransition::ContractUserModeration(_)
2389            | StateTransition::ContractFeeClaim(_)
2390            | StateTransition::Batch(_)
2391            | StateTransition::IdentityCreate(_)
2392            | StateTransition::IdentityTopUp(_)
2393            | StateTransition::IdentityCreditWithdrawal(_)
2394            | StateTransition::IdentityUpdate(_)
2395            | StateTransition::IdentityKeyLimitsUpdate(_)
2396            | StateTransition::IdentityCreditTransfer(_)
2397            | StateTransition::MasternodeVote(_) => {
2398                crate::validation::SimpleConsensusValidationResult::new_with_error(
2399                    UnsupportedFeatureError::new(
2400                        "structure validation for identity-based state transitions".to_string(),
2401                        platform_version.protocol_version,
2402                    )
2403                    .into(),
2404                )
2405            }
2406            StateTransition::IdentityCreditTransferToAddresses(transition) => {
2407                transition.validate_structure(platform_version)
2408            }
2409            StateTransition::IdentityCreateFromAddresses(transition) => {
2410                transition.validate_structure(platform_version)
2411            }
2412            StateTransition::IdentityTopUpFromAddresses(transition) => {
2413                transition.validate_structure(platform_version)
2414            }
2415            StateTransition::AddressFundsTransfer(transition) => {
2416                transition.validate_structure(platform_version)
2417            }
2418            StateTransition::AddressFundingFromAssetLock(transition) => {
2419                transition.validate_structure(platform_version)
2420            }
2421            StateTransition::AddressCreditWithdrawal(transition) => {
2422                transition.validate_structure(platform_version)
2423            }
2424            StateTransition::Shield(transition) => transition.validate_structure(platform_version),
2425            StateTransition::ShieldedTransfer(transition) => {
2426                transition.validate_structure(platform_version)
2427            }
2428            StateTransition::Unshield(transition) => {
2429                transition.validate_structure(platform_version)
2430            }
2431            StateTransition::IdentityTopUpFromShieldedPool(transition) => {
2432                transition.validate_structure(platform_version)
2433            }
2434            StateTransition::TokenShieldedTransferWithShieldedFee(transition) => {
2435                transition.validate_structure(platform_version)
2436            }
2437            StateTransition::TokenUnshieldWithShieldedFee(transition) => {
2438                transition.validate_structure(platform_version)
2439            }
2440            StateTransition::TokenPurchaseFromShieldedPool(transition) => {
2441                transition.validate_structure(platform_version)
2442            }
2443            StateTransition::ShieldFromAssetLock(transition) => {
2444                transition.validate_structure(platform_version)
2445            }
2446            StateTransition::ShieldedWithdrawal(transition) => {
2447                transition.validate_structure(platform_version)
2448            }
2449            StateTransition::IdentityCreateFromShieldedPool(transition) => {
2450                transition.validate_structure(platform_version)
2451            }
2452            StateTransition::ShieldFromIdentity(transition) => {
2453                transition.validate_structure(platform_version)
2454            }
2455        }
2456    }
2457}
2458
2459#[cfg(test)]
2460mod tests {
2461    use super::*;
2462
2463    // -----------------------------------------------------------------------
2464    // StateTransitionSigningOptions tests
2465    // -----------------------------------------------------------------------
2466
2467    #[test]
2468    fn test_signing_options_default() {
2469        let opts = StateTransitionSigningOptions::default();
2470        assert!(!opts.allow_signing_with_any_security_level);
2471        assert!(!opts.allow_signing_with_any_purpose);
2472    }
2473
2474    #[test]
2475    fn test_signing_options_equality() {
2476        let a = StateTransitionSigningOptions {
2477            allow_signing_with_any_security_level: true,
2478            allow_signing_with_any_purpose: false,
2479        };
2480        let b = StateTransitionSigningOptions {
2481            allow_signing_with_any_security_level: true,
2482            allow_signing_with_any_purpose: false,
2483        };
2484        assert_eq!(a, b);
2485    }
2486
2487    #[test]
2488    fn test_signing_options_inequality() {
2489        let a = StateTransitionSigningOptions {
2490            allow_signing_with_any_security_level: true,
2491            allow_signing_with_any_purpose: false,
2492        };
2493        let b = StateTransitionSigningOptions {
2494            allow_signing_with_any_security_level: false,
2495            allow_signing_with_any_purpose: false,
2496        };
2497        assert_ne!(a, b);
2498    }
2499
2500    #[test]
2501    #[allow(clippy::clone_on_copy)]
2502    fn test_signing_options_clone() {
2503        let original = StateTransitionSigningOptions {
2504            allow_signing_with_any_security_level: true,
2505            allow_signing_with_any_purpose: true,
2506        };
2507        let cloned = original.clone();
2508        assert_eq!(original, cloned);
2509    }
2510
2511    #[test]
2512    fn test_signing_options_copy() {
2513        let original = StateTransitionSigningOptions {
2514            allow_signing_with_any_security_level: true,
2515            allow_signing_with_any_purpose: false,
2516        };
2517        let copied = original;
2518        assert_eq!(original, copied);
2519    }
2520
2521    #[test]
2522    fn test_signing_options_debug() {
2523        let opts = StateTransitionSigningOptions::default();
2524        let debug_str = format!("{:?}", opts);
2525        assert!(debug_str.contains("StateTransitionSigningOptions"));
2526        assert!(debug_str.contains("allow_signing_with_any_security_level"));
2527        assert!(debug_str.contains("allow_signing_with_any_purpose"));
2528    }
2529
2530    // -----------------------------------------------------------------------
2531    // StateTransition enum accessor / mutator / classification tests
2532    //
2533    // These exercise the non-trivial match arms across the large enum, using
2534    // the IdentityCreditTransfer, MasternodeVote, IdentityCreditWithdrawal and
2535    // DataContractCreate variants as representative signed / unsigned /
2536    // voting / contract cases. They intentionally do NOT use `sign`/`verify`
2537    // (those go through BLS/ECDSA and have their own coverage elsewhere).
2538    // -----------------------------------------------------------------------
2539    use crate::identity::core_script::CoreScript;
2540    use crate::identity::{Purpose, SecurityLevel};
2541    use crate::prelude::Identifier;
2542    use crate::state_transition::identity_credit_transfer_transition::v0::IdentityCreditTransferTransitionV0;
2543    use crate::state_transition::identity_credit_transfer_transition::IdentityCreditTransferTransition;
2544    use crate::state_transition::identity_credit_withdrawal_transition::v0::IdentityCreditWithdrawalTransitionV0;
2545    use crate::state_transition::identity_credit_withdrawal_transition::IdentityCreditWithdrawalTransition;
2546    use crate::state_transition::masternode_vote_transition::v0::MasternodeVoteTransitionV0;
2547    use crate::state_transition::masternode_vote_transition::MasternodeVoteTransition;
2548    use crate::withdrawal::Pooling;
2549
2550    fn sample_transfer_st() -> StateTransition {
2551        let v0 = IdentityCreditTransferTransitionV0 {
2552            identity_id: Identifier::from([1u8; 32]),
2553            recipient_id: Identifier::from([2u8; 32]),
2554            amount: 1_000,
2555            nonce: 7,
2556            user_fee_increase: 3,
2557            signature_public_key_id: 11,
2558            signature: BinaryData::new(vec![0u8; 65]),
2559        };
2560        StateTransition::IdentityCreditTransfer(IdentityCreditTransferTransition::V0(v0))
2561    }
2562
2563    fn sample_masternode_vote_st() -> StateTransition {
2564        let v0 = MasternodeVoteTransitionV0 {
2565            pro_tx_hash: Identifier::from([3u8; 32]),
2566            voter_identity_id: Identifier::from([4u8; 32]),
2567            vote: Default::default(),
2568            nonce: 2,
2569            signature_public_key_id: 5,
2570            signature: BinaryData::new(vec![9u8; 10]),
2571        };
2572        StateTransition::MasternodeVote(MasternodeVoteTransition::V0(v0))
2573    }
2574
2575    fn sample_withdrawal_st() -> StateTransition {
2576        let v0 = IdentityCreditWithdrawalTransitionV0 {
2577            identity_id: Identifier::from([5u8; 32]),
2578            amount: 42,
2579            core_fee_per_byte: 1,
2580            pooling: Pooling::Never,
2581            output_script: CoreScript::from_bytes(vec![0x76, 0xa9]),
2582            nonce: 4,
2583            user_fee_increase: 1,
2584            signature_public_key_id: 3,
2585            signature: BinaryData::new(vec![8u8; 65]),
2586        };
2587        StateTransition::IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition::V0(v0))
2588    }
2589
2590    #[test]
2591    fn test_name_returns_variant_names() {
2592        assert_eq!(sample_transfer_st().name(), "IdentityCreditTransfer");
2593        assert_eq!(sample_masternode_vote_st().name(), "MasternodeVote");
2594        assert_eq!(sample_withdrawal_st().name(), "IdentityCreditWithdrawal");
2595    }
2596
2597    #[test]
2598    fn test_state_transition_type_matches_variant() {
2599        assert_eq!(
2600            sample_transfer_st().state_transition_type(),
2601            StateTransitionType::IdentityCreditTransfer
2602        );
2603        assert_eq!(
2604            sample_masternode_vote_st().state_transition_type(),
2605            StateTransitionType::MasternodeVote
2606        );
2607        assert_eq!(
2608            sample_withdrawal_st().state_transition_type(),
2609            StateTransitionType::IdentityCreditWithdrawal
2610        );
2611    }
2612
2613    #[test]
2614    fn test_is_identity_signed_excludes_asset_lock_and_shielded() {
2615        assert!(sample_transfer_st().is_identity_signed());
2616        assert!(sample_masternode_vote_st().is_identity_signed());
2617        assert!(sample_withdrawal_st().is_identity_signed());
2618    }
2619
2620    #[test]
2621    fn test_signature_accessor() {
2622        let st = sample_transfer_st();
2623        let sig = st.signature().expect("transfer should expose signature");
2624        assert_eq!(sig.len(), 65);
2625
2626        let st = sample_masternode_vote_st();
2627        let sig = st.signature().expect("masternode vote has signature");
2628        assert_eq!(sig.as_slice(), &[9u8; 10]);
2629    }
2630
2631    #[test]
2632    fn test_owner_id_accessor() {
2633        let transfer = sample_transfer_st();
2634        assert_eq!(transfer.owner_id(), Some(Identifier::from([1u8; 32])));
2635
2636        let vote = sample_masternode_vote_st();
2637        assert_eq!(vote.owner_id(), Some(Identifier::from([4u8; 32])));
2638
2639        let withdraw = sample_withdrawal_st();
2640        assert_eq!(withdraw.owner_id(), Some(Identifier::from([5u8; 32])));
2641    }
2642
2643    #[test]
2644    fn test_signature_public_key_id_accessor() {
2645        assert_eq!(sample_transfer_st().signature_public_key_id(), Some(11));
2646        assert_eq!(
2647            sample_masternode_vote_st().signature_public_key_id(),
2648            Some(5)
2649        );
2650        assert_eq!(sample_withdrawal_st().signature_public_key_id(), Some(3));
2651    }
2652
2653    #[test]
2654    fn test_user_fee_increase_for_various_variants() {
2655        // Transfer exposes its internal value.
2656        assert_eq!(sample_transfer_st().user_fee_increase(), 3);
2657        // Masternode vote returns 0 unconditionally.
2658        assert_eq!(sample_masternode_vote_st().user_fee_increase(), 0);
2659        // Withdrawal exposes its internal value.
2660        assert_eq!(sample_withdrawal_st().user_fee_increase(), 1);
2661    }
2662
2663    #[test]
2664    fn test_set_signature_returns_true_for_supported() {
2665        let mut st = sample_transfer_st();
2666        let ok = st.set_signature(BinaryData::new(vec![0xaa; 65]));
2667        assert!(ok);
2668        assert_eq!(st.signature().unwrap().as_slice(), &[0xaa; 65]);
2669    }
2670
2671    #[test]
2672    fn test_set_user_fee_increase_updates_value() {
2673        let mut st = sample_transfer_st();
2674        st.set_user_fee_increase(42);
2675        assert_eq!(st.user_fee_increase(), 42);
2676
2677        // Masternode vote ignores the setter (documented no-op) — still reads 0.
2678        let mut vote = sample_masternode_vote_st();
2679        vote.set_user_fee_increase(99);
2680        assert_eq!(vote.user_fee_increase(), 0);
2681    }
2682
2683    #[test]
2684    fn test_set_signature_public_key_id() {
2685        let mut st = sample_transfer_st();
2686        st.set_signature_public_key_id(1234);
2687        assert_eq!(st.signature_public_key_id(), Some(1234));
2688    }
2689
2690    #[test]
2691    fn test_required_number_of_private_keys_default() {
2692        // Non asset-lock transitions always require 1 key.
2693        assert_eq!(sample_transfer_st().required_number_of_private_keys(), 1);
2694        assert_eq!(
2695            sample_masternode_vote_st().required_number_of_private_keys(),
2696            1
2697        );
2698        assert_eq!(sample_withdrawal_st().required_number_of_private_keys(), 1);
2699    }
2700
2701    #[test]
2702    fn test_inputs_none_for_legacy_variants() {
2703        // All these variants have no PlatformAddress inputs.
2704        assert!(sample_transfer_st().inputs().is_none());
2705        assert!(sample_masternode_vote_st().inputs().is_none());
2706        assert!(sample_withdrawal_st().inputs().is_none());
2707    }
2708
2709    #[test]
2710    fn test_active_version_range_legacy_transitions() {
2711        // These all report ALL_VERSIONS per the mod.rs table.
2712        assert_eq!(sample_transfer_st().active_version_range(), ALL_VERSIONS);
2713        assert_eq!(
2714            sample_masternode_vote_st().active_version_range(),
2715            ALL_VERSIONS
2716        );
2717        assert_eq!(sample_withdrawal_st().active_version_range(), ALL_VERSIONS);
2718    }
2719
2720    #[test]
2721    fn test_unique_identifiers_non_empty() {
2722        let ids = sample_transfer_st().unique_identifiers();
2723        assert_eq!(ids.len(), 1);
2724        assert!(!ids[0].is_empty());
2725    }
2726
2727    #[test]
2728    fn test_required_asset_lock_balance_rejects_non_asset_lock() {
2729        let platform_version = PlatformVersion::latest();
2730        let st = sample_transfer_st();
2731        let err = st
2732            .required_asset_lock_balance_for_processing_start(platform_version)
2733            .expect_err("credit transfer is not an asset lock state transition");
2734        match err {
2735            ProtocolError::CorruptedCodeExecution(msg) => {
2736                assert!(
2737                    msg.contains("is not an asset lock transaction"),
2738                    "unexpected error message: {msg}"
2739                );
2740            }
2741            other => panic!("expected CorruptedCodeExecution, got {other:?}"),
2742        }
2743    }
2744
2745    #[test]
2746    fn test_security_level_requirement_for_transfer() {
2747        // IdentityCreditTransfer requires CRITICAL at TRANSFER purpose.
2748        let st = sample_transfer_st();
2749        let levels = st
2750            .security_level_requirement(Purpose::TRANSFER)
2751            .expect("transfer state transition should return a requirement");
2752        assert_eq!(levels, vec![SecurityLevel::CRITICAL]);
2753    }
2754
2755    #[test]
2756    fn test_purpose_requirement_for_transfer() {
2757        let st = sample_transfer_st();
2758        let purposes = st
2759            .purpose_requirement()
2760            .expect("transfer state transition should have a purpose");
2761        assert_eq!(purposes, vec![Purpose::TRANSFER]);
2762    }
2763
2764    #[test]
2765    fn test_optional_asset_lock_proof_none_for_transfer() {
2766        let st = sample_transfer_st();
2767        assert!(st.optional_asset_lock_proof().is_none());
2768    }
2769
2770    // -----------------------------------------------------------------------
2771    // Enum construction: From<V0 / outer enum> → StateTransition
2772    // -----------------------------------------------------------------------
2773
2774    #[test]
2775    fn test_from_outer_enum_into_state_transition() {
2776        let outer: IdentityCreditTransferTransition =
2777            IdentityCreditTransferTransition::V0(IdentityCreditTransferTransitionV0::default());
2778        let st: StateTransition = outer.into();
2779        assert!(matches!(st, StateTransition::IdentityCreditTransfer(_)));
2780    }
2781
2782    #[test]
2783    fn test_from_masternode_vote_outer_into_state_transition() {
2784        let outer: MasternodeVoteTransition =
2785            MasternodeVoteTransition::V0(MasternodeVoteTransitionV0::default());
2786        let st: StateTransition = outer.into();
2787        assert!(matches!(st, StateTransition::MasternodeVote(_)));
2788    }
2789
2790    // -----------------------------------------------------------------------
2791    // Serialization round-trip: platform serialize / deserialize via enum.
2792    // Exercises the top-level `StateTransition` (de)serialize glue.
2793    // -----------------------------------------------------------------------
2794
2795    #[test]
2796    fn test_state_transition_platform_serialize_roundtrip() {
2797        use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable};
2798        let original = sample_transfer_st();
2799        let bytes =
2800            PlatformSerializable::serialize_to_bytes(&original).expect("serialize should succeed");
2801        let restored = StateTransition::deserialize_from_bytes_untrusted(&bytes)
2802            .expect("deserialize should succeed");
2803        assert_eq!(original, restored);
2804    }
2805
2806    #[test]
2807    fn test_deserialize_from_bytes_in_version_succeeds_for_latest() {
2808        use crate::serialization::PlatformSerializable;
2809        let original = sample_transfer_st();
2810        let bytes =
2811            PlatformSerializable::serialize_to_bytes(&original).expect("serialize succeeds");
2812        let restored = StateTransition::deserialize_from_bytes_untrusted_in_version(
2813            &bytes,
2814            PlatformVersion::latest(),
2815        )
2816        .expect("deserialize_from_bytes_in_version should succeed");
2817        assert_eq!(original, restored);
2818    }
2819
2820    #[test]
2821    fn should_refuse_bytes_after_the_transition_only_in_the_exact_in_version_decoder() {
2822        use crate::serialization::PlatformSerializable;
2823        let original = sample_transfer_st();
2824        let bytes =
2825            PlatformSerializable::serialize_to_bytes(&original).expect("serialize succeeds");
2826        let platform_version = PlatformVersion::latest();
2827
2828        assert_eq!(
2829            StateTransition::deserialize_from_bytes_untrusted_exact_in_version(
2830                &bytes,
2831                platform_version
2832            )
2833            .expect("the exact bytes decode"),
2834            original
2835        );
2836
2837        for left_over in [1usize, 100] {
2838            let mut padded = bytes.clone();
2839            padded.extend(std::iter::repeat_n(0xAB, left_over));
2840
2841            assert_eq!(
2842                StateTransition::deserialize_from_bytes_untrusted_in_version(
2843                    &padded,
2844                    platform_version
2845                )
2846                .expect("the loose decoder ignores the suffix"),
2847                original
2848            );
2849            match StateTransition::deserialize_from_bytes_untrusted_exact_in_version(
2850                &padded,
2851                platform_version,
2852            ) {
2853                Err(ProtocolError::PlatformDeserializationError(message)) => assert!(
2854                    message.contains(&format!("{left_over} bytes left over")),
2855                    "unexpected message: {message}"
2856                ),
2857                other => panic!("expected a deserialization error, got {other:?}"),
2858            }
2859        }
2860    }
2861
2862    #[test]
2863    fn test_transaction_id_is_deterministic() {
2864        let st = sample_transfer_st();
2865        let a = st.transaction_id().expect("hash should succeed");
2866        let b = st.transaction_id().expect("hash should succeed");
2867        assert_eq!(a, b);
2868        assert_eq!(a.len(), 32);
2869    }
2870
2871    #[test]
2872    fn test_transaction_id_changes_on_signature_change() {
2873        let mut st = sample_transfer_st();
2874        let before = st.transaction_id().expect("hash should succeed");
2875        st.set_signature(BinaryData::new(vec![0xbb; 65]));
2876        let after = st.transaction_id().expect("hash should succeed");
2877        // Different signatures produce a different serialized form.
2878        assert_ne!(before, after);
2879    }
2880
2881    #[test]
2882    fn test_clone_preserves_inner_state() {
2883        let st = sample_transfer_st();
2884        let cloned = st.clone();
2885        assert_eq!(st, cloned);
2886    }
2887
2888    // -----------------------------------------------------------------------
2889    // Additional coverage: enum arms that weren't previously exercised.
2890    //
2891    // The tests below intentionally target variants the earlier tests did not
2892    // touch (DataContractCreate, DataContractUpdate, Batch, IdentityCreate,
2893    // IdentityTopUp, IdentityUpdate, shielded / address variants) to cover
2894    // the remaining match-arm branches in accessor / mutator / classification
2895    // methods.
2896    // -----------------------------------------------------------------------
2897
2898    use crate::data_contract::serialized_version::DataContractInSerializationFormat;
2899    use crate::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0;
2900    use crate::state_transition::batch_transition::document_base_transition::DocumentBaseTransition;
2901    use crate::state_transition::batch_transition::document_delete_transition::{
2902        DocumentDeleteTransition, DocumentDeleteTransitionV0,
2903    };
2904    use crate::state_transition::batch_transition::{BatchTransition, BatchTransitionV0};
2905    use crate::state_transition::data_contract_create_transition::{
2906        DataContractCreateTransition, DataContractCreateTransitionV0,
2907        DataContractCreateTransitionV1,
2908    };
2909    use crate::state_transition::data_contract_update_transition::{
2910        DataContractUpdateTransition, DataContractUpdateTransitionV0,
2911    };
2912    use crate::state_transition::identity_create_transition::v0::IdentityCreateTransitionV0;
2913    use crate::state_transition::identity_create_transition::IdentityCreateTransition;
2914    use crate::state_transition::identity_topup_transition::v0::IdentityTopUpTransitionV0;
2915    use crate::state_transition::identity_topup_transition::IdentityTopUpTransition;
2916    use crate::state_transition::identity_update_transition::v0::IdentityUpdateTransitionV0;
2917    use crate::state_transition::identity_update_transition::IdentityUpdateTransition;
2918    use crate::state_transition::shielded_transfer_transition::v0::ShieldedTransferTransitionV0;
2919    use crate::state_transition::shielded_transfer_transition::ShieldedTransferTransition;
2920    use crate::state_transition::shielded_withdrawal_transition::v0::ShieldedWithdrawalTransitionV0;
2921    use crate::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition;
2922    use crate::state_transition::unshield_transition::v0::UnshieldTransitionV0;
2923    use crate::state_transition::unshield_transition::UnshieldTransition;
2924
2925    /// Build a DataContractInSerializationFormat from a crate-private v0
2926    /// constructor via the public TryFromPlatformVersioned impl and DataContract V1.
2927    fn sample_data_contract_in_serialization_format() -> DataContractInSerializationFormat {
2928        use crate::data_contract::config::v0::DataContractConfigV0;
2929        use crate::data_contract::config::DataContractConfig;
2930        use crate::data_contract::v1::DataContractV1;
2931        use crate::data_contract::DataContract;
2932        use platform_version::TryIntoPlatformVersioned;
2933        use std::collections::BTreeMap;
2934
2935        let contract = DataContract::V1(DataContractV1 {
2936            id: Identifier::from([9u8; 32]),
2937            version: 1,
2938            owner_id: Identifier::from([7u8; 32]),
2939            document_types: BTreeMap::new(),
2940            config: DataContractConfig::V0(DataContractConfigV0 {
2941                can_be_deleted: false,
2942                readonly: false,
2943                keeps_history: false,
2944                documents_keep_history_contract_default: false,
2945                documents_mutable_contract_default: false,
2946                documents_can_be_deleted_contract_default: false,
2947                requires_identity_encryption_bounded_key: None,
2948                requires_identity_decryption_bounded_key: None,
2949            }),
2950            schema_defs: None,
2951            created_at: None,
2952            updated_at: None,
2953            created_at_block_height: None,
2954            updated_at_block_height: None,
2955            created_at_epoch: None,
2956            updated_at_epoch: None,
2957            groups: BTreeMap::new(),
2958            tokens: BTreeMap::new(),
2959            keywords: Vec::new(),
2960            description: None,
2961        });
2962
2963        contract
2964            .try_into_platform_versioned(PlatformVersion::latest())
2965            .expect("expected to serialize a trivial contract")
2966    }
2967
2968    fn sample_data_contract_create_st() -> StateTransition {
2969        StateTransition::DataContractCreate(DataContractCreateTransition::V0(
2970            DataContractCreateTransitionV0 {
2971                data_contract: sample_data_contract_in_serialization_format(),
2972                identity_nonce: 1,
2973                user_fee_increase: 5,
2974                signature_public_key_id: 2,
2975                signature: BinaryData::new(vec![0xAB; 65]),
2976            },
2977        ))
2978    }
2979
2980    fn sample_data_contract_create_v1_st() -> StateTransition {
2981        StateTransition::DataContractCreate(DataContractCreateTransition::V1(
2982            DataContractCreateTransitionV1 {
2983                data_contract: sample_data_contract_in_serialization_format(),
2984                identity_nonce: 1,
2985                contract_group: None,
2986                contract_group_memberships: vec![],
2987                user_fee_increase: 5,
2988                signature_public_key_id: 2,
2989                signature: BinaryData::new(vec![0xAB; 65]),
2990            },
2991        ))
2992    }
2993
2994    fn sample_data_contract_update_st() -> StateTransition {
2995        StateTransition::DataContractUpdate(DataContractUpdateTransition::V0(
2996            DataContractUpdateTransitionV0 {
2997                identity_contract_nonce: 4,
2998                data_contract: sample_data_contract_in_serialization_format(),
2999                user_fee_increase: 9,
3000                signature_public_key_id: 6,
3001                signature: BinaryData::new(vec![0xCD; 65]),
3002            },
3003        ))
3004    }
3005
3006    #[cfg(all(feature = "state-transition-signing", feature = "bls-signatures"))]
3007    #[test]
3008    fn should_enforce_contract_bounds_before_private_key_signing() {
3009        use crate::consensus::signature::{
3010            ContractBoundedKeyNonBatchError, ContractBoundedKeyOutOfBoundsError,
3011        };
3012        use crate::identity::contract_bounds::ContractBounds;
3013        use crate::identity::identity_public_key::v0::IdentityPublicKeyV0;
3014
3015        let private_key = [1; 32];
3016        let bls = crate::bls::native_bls::NativeBlsModule;
3017        // `sample_batch_st_with_delete` deletes a "preorder" document of contract [2; 32].
3018        let mut key = IdentityPublicKeyV0 {
3019            id: 7,
3020            purpose: Purpose::AUTHENTICATION,
3021            security_level: SecurityLevel::HIGH,
3022            key_type: KeyType::ECDSA_SECP256K1,
3023            data: get_compressed_public_ec_key(&private_key)
3024                .unwrap()
3025                .to_vec()
3026                .into(),
3027            contract_bounds: Some(ContractBounds::SingleContract {
3028                id: Identifier::from([2; 32]),
3029            }),
3030            ..Default::default()
3031        };
3032        sample_batch_st_with_delete()
3033            .sign(&key.clone().into(), &private_key, &bls)
3034            .expect("a batch inside the bounds must sign");
3035        key.contract_bounds = Some(ContractBounds::SingleContractDocumentType {
3036            id: Identifier::from([2; 32]),
3037            document_type_name: "preorder".to_string(),
3038        });
3039        sample_batch_st_with_delete()
3040            .sign(&key.clone().into(), &private_key, &bls)
3041            .expect("a batch of the bound document type must sign");
3042
3043        let err = sample_transfer_st()
3044            .sign(&key.clone().into(), &private_key, &bls)
3045            .unwrap_err();
3046        assert!(matches!(err, ProtocolError::ConsensusError(error)
3047            if *error == ContractBoundedKeyNonBatchError::new(key.id).into()));
3048
3049        for denied in [
3050            ContractBounds::SingleContract {
3051                id: Identifier::from([3; 32]),
3052            },
3053            ContractBounds::SingleContractDocumentType {
3054                id: Identifier::from([2; 32]),
3055                document_type_name: "other".to_string(),
3056            },
3057        ] {
3058            key.contract_bounds = Some(denied.clone());
3059            let mut transition = sample_batch_st_with_delete();
3060            let original = transition.clone();
3061            let err = transition
3062                .sign(&key.clone().into(), &private_key, &bls)
3063                .unwrap_err();
3064            assert!(
3065                matches!(err, ProtocolError::ConsensusError(error)
3066                if *error == ContractBoundedKeyOutOfBoundsError::new(key.id).into()),
3067                "{denied:?}"
3068            );
3069            assert_eq!(
3070                transition, original,
3071                "rejection must preserve the transition"
3072            );
3073        }
3074
3075        key.contract_bounds = None;
3076        sample_batch_st_with_delete()
3077            .sign(&key.into(), &private_key, &bls)
3078            .expect("unbounded keys must still sign");
3079    }
3080
3081    // Generation 1 of `verify_identity_signed_signature` (protocol version 14) refuses a
3082    // BLS12_381 signature that does not verify; generation 0 keeps accepting it for replay.
3083    #[cfg(all(feature = "state-transition-signing", feature = "bls-signatures"))]
3084    #[test]
3085    fn should_refuse_a_bls_signature_that_does_not_verify_from_protocol_version_14() {
3086        use crate::identity::identity_public_key::v0::IdentityPublicKeyV0;
3087
3088        let bls = crate::bls::native_bls::NativeBlsModule;
3089        let private_key = [7; 32];
3090        let key: IdentityPublicKey = IdentityPublicKeyV0 {
3091            id: 11,
3092            purpose: Purpose::TRANSFER,
3093            security_level: SecurityLevel::CRITICAL,
3094            key_type: KeyType::BLS12_381,
3095            data: bls
3096                .private_key_to_public_key(&private_key)
3097                .expect("a valid BLS private key")
3098                .into(),
3099            ..Default::default()
3100        }
3101        .into();
3102
3103        let mut signed = sample_transfer_st();
3104        signed
3105            .sign(&key, &private_key, &bls)
3106            .expect("the key signs the transfer");
3107
3108        // The same transfer signed by another BLS key, and 96 bytes that are not a compressed
3109        // curve point
3110        let mut by_another_key = signed.clone();
3111        by_another_key
3112            .sign_by_private_key(&[8; 32], KeyType::BLS12_381, &bls)
3113            .expect("another key signs the transfer");
3114        let mut not_a_point = signed.clone();
3115        not_a_point.set_signature(BinaryData::new(vec![0; 96]));
3116
3117        let latest = PlatformVersion::latest();
3118        let last_without_the_check = PlatformVersion::get(13).expect("protocol version 13");
3119        for platform_version in [latest, last_without_the_check] {
3120            signed
3121                .verify_identity_signed_signature(&key, &bls, platform_version)
3122                .expect("a signature by the key verifies");
3123        }
3124
3125        for (transition, label) in [
3126            (by_another_key, "signed by another key"),
3127            (not_a_point, "not a curve point"),
3128        ] {
3129            let error = transition
3130                .verify_identity_signed_signature(&key, &bls, latest)
3131                .expect_err(label);
3132            assert!(
3133                matches!(&error, ProtocolError::ConsensusError(consensus_error)
3134                if matches!(&**consensus_error, ConsensusError::SignatureError(
3135                    SignatureError::InvalidStateTransitionSignatureError(e)
3136                ) if e.message() == "BLS12_381 signature does not verify")),
3137                "{label}: {error:?}"
3138            );
3139            transition
3140                .verify_identity_signed_signature(&key, &bls, last_without_the_check)
3141                .expect("protocol version 13 replays as it ran");
3142        }
3143    }
3144
3145    fn sample_batch_st_with_delete() -> StateTransition {
3146        let base = DocumentBaseTransition::V0(DocumentBaseTransitionV0 {
3147            id: Identifier::from([1u8; 32]),
3148            identity_contract_nonce: 3,
3149            document_type_name: "preorder".to_string(),
3150            data_contract_id: Identifier::from([2u8; 32]),
3151        });
3152        let delete =
3153            DocumentTransition::Delete(DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 {
3154                base,
3155            }));
3156        StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 {
3157            owner_id: Identifier::from([8u8; 32]),
3158            transitions: vec![delete],
3159            user_fee_increase: 2,
3160            signature_public_key_id: 7,
3161            signature: BinaryData::new(vec![0xEE; 65]),
3162        }))
3163    }
3164
3165    fn sample_batch_st_with_base(base: DocumentBaseTransition) -> StateTransition {
3166        let delete =
3167            DocumentTransition::Delete(DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 {
3168                base,
3169            }));
3170        StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 {
3171            owner_id: Identifier::from([8u8; 32]),
3172            transitions: vec![delete],
3173            user_fee_increase: 2,
3174            signature_public_key_id: 7,
3175            signature: BinaryData::new(vec![0xEE; 65]),
3176        }))
3177    }
3178
3179    // Version 2 of the document base carries the action fee agreement, which exists from
3180    // protocol version 14. Software from before it cannot decode the base, so new software
3181    // must refuse the batch while an earlier version is active; version 1, which every
3182    // version since 9 builds, stays active everywhere its batch is.
3183    #[test]
3184    fn should_not_activate_a_version_2_document_base_before_protocol_version_14() {
3185        use crate::serialization::PlatformSerializable;
3186        use crate::state_transition::batch_transition::document_base_transition::v1::DocumentBaseTransitionV1;
3187        use crate::state_transition::batch_transition::document_base_transition::v2::DocumentBaseTransitionV2;
3188
3189        let version_1 =
3190            sample_batch_st_with_base(DocumentBaseTransition::V1(DocumentBaseTransitionV1 {
3191                id: Identifier::from([1u8; 32]),
3192                identity_contract_nonce: 3,
3193                document_type_name: "preorder".to_string(),
3194                data_contract_id: Identifier::from([2u8; 32]),
3195                token_payment_info: None,
3196            }));
3197        assert_eq!(version_1.active_version_range(), ALL_VERSIONS);
3198
3199        let version_2 =
3200            sample_batch_st_with_base(DocumentBaseTransition::V2(DocumentBaseTransitionV2 {
3201                id: Identifier::from([1u8; 32]),
3202                identity_contract_nonce: 3,
3203                document_type_name: "preorder".to_string(),
3204                data_contract_id: Identifier::from([2u8; 32]),
3205                token_payment_info: None,
3206                action_fee_agreement: None,
3207            }));
3208        assert_eq!(version_2.active_version_range(), 14..=LATEST_VERSION);
3209
3210        let bytes =
3211            PlatformSerializable::serialize_to_bytes(&version_2).expect("serialize succeeds");
3212        let version_13 = PlatformVersion::get(13).expect("platform version 13 exists");
3213        assert!(matches!(
3214            StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, version_13),
3215            Err(ProtocolError::StateTransitionError(
3216                StateTransitionIsNotActiveError { .. }
3217            ))
3218        ));
3219        let recovered = StateTransition::deserialize_from_bytes_untrusted_in_version(
3220            &bytes,
3221            PlatformVersion::latest(),
3222        )
3223        .expect("expected protocol version 14 to decode a version 2 base");
3224        assert_eq!(recovered, version_2);
3225    }
3226
3227    fn sample_batch_st_empty() -> StateTransition {
3228        StateTransition::Batch(BatchTransition::V0(BatchTransitionV0 {
3229            owner_id: Identifier::from([1u8; 32]),
3230            transitions: vec![],
3231            user_fee_increase: 0,
3232            signature_public_key_id: 0,
3233            signature: BinaryData::new(vec![]),
3234        }))
3235    }
3236
3237    fn sample_identity_create_st() -> StateTransition {
3238        StateTransition::IdentityCreate(IdentityCreateTransition::V0(IdentityCreateTransitionV0 {
3239            identity_id: Identifier::from([3u8; 32]),
3240            ..Default::default()
3241        }))
3242    }
3243
3244    fn sample_identity_top_up_st() -> StateTransition {
3245        StateTransition::IdentityTopUp(IdentityTopUpTransition::V0(IdentityTopUpTransitionV0 {
3246            identity_id: Identifier::from([4u8; 32]),
3247            ..Default::default()
3248        }))
3249    }
3250
3251    fn sample_identity_update_st() -> StateTransition {
3252        StateTransition::IdentityUpdate(IdentityUpdateTransition::V0(IdentityUpdateTransitionV0 {
3253            identity_id: Identifier::from([5u8; 32]),
3254            revision: 1,
3255            nonce: 2,
3256            add_public_keys: vec![],
3257            disable_public_keys: vec![],
3258            user_fee_increase: 11,
3259            signature_public_key_id: 33,
3260            signature: BinaryData::new(vec![0xFF; 65]),
3261        }))
3262    }
3263
3264    fn sample_unshield_st() -> StateTransition {
3265        StateTransition::Unshield(UnshieldTransition::V0(UnshieldTransitionV0 {
3266            output_address: Default::default(),
3267            actions: vec![],
3268            unshielding_amount: 0,
3269            anchor: [0u8; 32],
3270            proof: vec![],
3271            binding_signature: [0u8; 64],
3272        }))
3273    }
3274
3275    fn sample_shielded_transfer_st() -> StateTransition {
3276        StateTransition::ShieldedTransfer(ShieldedTransferTransition::V0(
3277            ShieldedTransferTransitionV0 {
3278                actions: vec![],
3279                value_balance: 0,
3280                anchor: [0u8; 32],
3281                proof: vec![],
3282                binding_signature: [0u8; 64],
3283            },
3284        ))
3285    }
3286
3287    fn sample_shielded_withdrawal_st() -> StateTransition {
3288        use crate::identity::core_script::CoreScript;
3289        use crate::withdrawal::Pooling;
3290        StateTransition::ShieldedWithdrawal(ShieldedWithdrawalTransition::V0(
3291            ShieldedWithdrawalTransitionV0 {
3292                actions: vec![],
3293                unshielding_amount: 0,
3294                anchor: [0u8; 32],
3295                proof: vec![],
3296                binding_signature: [0u8; 64],
3297                core_fee_per_byte: 1,
3298                pooling: Pooling::Never,
3299                output_script: CoreScript::from_bytes(vec![]),
3300            },
3301        ))
3302    }
3303
3304    // --- name() covers all previously-untested arms, including the nested
3305    // match for Batch variants. ---
3306    #[test]
3307    fn test_name_for_newly_covered_variants() {
3308        assert_eq!(
3309            sample_data_contract_create_st().name(),
3310            "DataContractCreate"
3311        );
3312        assert_eq!(
3313            sample_data_contract_update_st().name(),
3314            "DataContractUpdate"
3315        );
3316        assert_eq!(sample_identity_create_st().name(), "IdentityCreate");
3317        assert_eq!(sample_identity_top_up_st().name(), "IdentityTopUp");
3318        assert_eq!(sample_identity_update_st().name(), "IdentityUpdate");
3319        assert_eq!(sample_unshield_st().name(), "Unshield");
3320        assert_eq!(sample_shielded_transfer_st().name(), "ShieldedTransfer");
3321        assert_eq!(sample_shielded_withdrawal_st().name(), "ShieldedWithdrawal");
3322
3323        // Batch with a single Delete – exercises the nested DocumentTransition
3324        // match arm in `name()`.
3325        let batch_name = sample_batch_st_with_delete().name();
3326        assert_eq!(batch_name, "DocumentsBatch([Delete])");
3327
3328        // Empty batch – still renders, with an empty list.
3329        let empty_name = sample_batch_st_empty().name();
3330        assert_eq!(empty_name, "DocumentsBatch([])");
3331    }
3332
3333    // --- state_transition_type covers the call_method! dispatch. ---
3334    #[test]
3335    fn test_state_transition_type_for_newly_covered_variants() {
3336        assert_eq!(
3337            sample_data_contract_create_st().state_transition_type(),
3338            StateTransitionType::DataContractCreate
3339        );
3340        assert_eq!(
3341            sample_data_contract_update_st().state_transition_type(),
3342            StateTransitionType::DataContractUpdate
3343        );
3344        assert_eq!(
3345            sample_batch_st_with_delete().state_transition_type(),
3346            StateTransitionType::Batch
3347        );
3348        assert_eq!(
3349            sample_identity_create_st().state_transition_type(),
3350            StateTransitionType::IdentityCreate
3351        );
3352        assert_eq!(
3353            sample_identity_top_up_st().state_transition_type(),
3354            StateTransitionType::IdentityTopUp
3355        );
3356        assert_eq!(
3357            sample_identity_update_st().state_transition_type(),
3358            StateTransitionType::IdentityUpdate
3359        );
3360        assert_eq!(
3361            sample_unshield_st().state_transition_type(),
3362            StateTransitionType::Unshield
3363        );
3364        assert_eq!(
3365            sample_shielded_transfer_st().state_transition_type(),
3366            StateTransitionType::ShieldedTransfer
3367        );
3368        assert_eq!(
3369            sample_shielded_withdrawal_st().state_transition_type(),
3370            StateTransitionType::ShieldedWithdrawal
3371        );
3372    }
3373
3374    // --- active_version_range uses different branches per transition
3375    // "group". Exercises the contract-format V1 branch for
3376    // DataContractCreate/Update (9..=LATEST), the BatchTransitionV0 branch
3377    // (ALL_VERSIONS), and the shielded range (12..=LATEST).
3378    #[test]
3379    fn test_active_version_range_contract_and_shielded_branches() {
3380        // DataContractCreate/Update on PlatformVersion::latest use the V1
3381        // contract serialization format, which restricts active range.
3382        let contract_v1_range = 9..=LATEST_VERSION;
3383        assert_eq!(
3384            sample_data_contract_create_st().active_version_range(),
3385            contract_v1_range
3386        );
3387        let contract_v1_range = 9..=LATEST_VERSION;
3388        assert_eq!(
3389            sample_data_contract_update_st().active_version_range(),
3390            contract_v1_range
3391        );
3392        // BatchTransition::V0 → ALL_VERSIONS
3393        assert_eq!(
3394            sample_batch_st_with_delete().active_version_range(),
3395            ALL_VERSIONS
3396        );
3397        // IdentityCreate/TopUp/Update are ALL_VERSIONS.
3398        assert_eq!(
3399            sample_identity_create_st().active_version_range(),
3400            ALL_VERSIONS
3401        );
3402        assert_eq!(
3403            sample_identity_top_up_st().active_version_range(),
3404            ALL_VERSIONS
3405        );
3406        assert_eq!(
3407            sample_identity_update_st().active_version_range(),
3408            ALL_VERSIONS
3409        );
3410        // Shielded variants report a shielded range (12..=LATEST_VERSION).
3411        let shielded_range = 12..=LATEST_VERSION;
3412        assert_eq!(
3413            sample_shielded_transfer_st().active_version_range(),
3414            shielded_range.clone()
3415        );
3416        assert_eq!(
3417            sample_unshield_st().active_version_range(),
3418            shielded_range.clone()
3419        );
3420        assert_eq!(
3421            sample_shielded_withdrawal_st().active_version_range(),
3422            shielded_range
3423        );
3424    }
3425
3426    fn sample_identity_update_st_adding(
3427        public_key: crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation,
3428    ) -> StateTransition {
3429        StateTransition::IdentityUpdate(IdentityUpdateTransition::V0(IdentityUpdateTransitionV0 {
3430            identity_id: Identifier::from([5u8; 32]),
3431            revision: 1,
3432            nonce: 2,
3433            add_public_keys: vec![public_key],
3434            disable_public_keys: vec![],
3435            user_fee_increase: 0,
3436            signature_public_key_id: 0,
3437            signature: BinaryData::new(vec![0xFF; 65]),
3438        }))
3439    }
3440
3441    #[test]
3442    fn should_only_admit_a_version_1_public_key_in_creation_from_protocol_version_14() {
3443        use crate::serialization::PlatformSerializable;
3444        use crate::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0;
3445        use crate::state_transition::public_key_in_creation::v1::IdentityPublicKeyInCreationV1;
3446
3447        let version_0_key = IdentityPublicKeyInCreationV0 {
3448            id: 1,
3449            data: BinaryData::new(vec![2; 33]),
3450            ..Default::default()
3451        };
3452        let limited_key = IdentityPublicKeyInCreationV1::from_v0_with_limits(
3453            version_0_key.clone(),
3454            Some(1_000),
3455            None,
3456        );
3457
3458        let plain = sample_identity_update_st_adding(version_0_key.into());
3459        assert_eq!(plain.active_version_range(), ALL_VERSIONS);
3460
3461        let limited = sample_identity_update_st_adding(limited_key.into());
3462        assert_eq!(limited.active_version_range(), 14..=LATEST_VERSION);
3463
3464        // Protocol version 13 refuses it while decoding, the way a binary that does not know
3465        // the version 1 key does; protocol version 14 decodes it.
3466        let bytes = limited.serialize_to_bytes().expect("expected to serialize");
3467        let version_13 = PlatformVersion::get(13).expect("expected protocol version 13");
3468        assert!(matches!(
3469            StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, version_13),
3470            Err(ProtocolError::StateTransitionError(
3471                StateTransitionIsNotActiveError { .. }
3472            ))
3473        ));
3474        let version_14 = PlatformVersion::get(14).expect("expected protocol version 14");
3475        assert_eq!(
3476            StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, version_14)
3477                .expect("expected protocol version 14 to decode a limited key"),
3478            limited
3479        );
3480    }
3481
3482    // --- is_identity_signed exercises the inverted-match logic for the
3483    // shielded / identity-create / topup variants. ---
3484    #[test]
3485    fn test_is_identity_signed_false_for_identity_create_topup_and_shielded() {
3486        assert!(!sample_identity_create_st().is_identity_signed());
3487        assert!(!sample_identity_top_up_st().is_identity_signed());
3488        assert!(!sample_unshield_st().is_identity_signed());
3489        assert!(!sample_shielded_transfer_st().is_identity_signed());
3490        assert!(!sample_shielded_withdrawal_st().is_identity_signed());
3491    }
3492
3493    // --- signature accessor for each arm that returns Some/None; previously
3494    // only IdentityCreditTransfer / MasternodeVote / IdentityCreditWithdrawal
3495    // were covered.
3496    #[test]
3497    fn test_signature_accessor_for_other_variants() {
3498        // Some(_) arms
3499        assert_eq!(
3500            sample_data_contract_create_st().signature().unwrap().len(),
3501            65
3502        );
3503        assert_eq!(
3504            sample_data_contract_update_st().signature().unwrap().len(),
3505            65
3506        );
3507        assert_eq!(sample_batch_st_with_delete().signature().unwrap().len(), 65);
3508        assert_eq!(sample_identity_update_st().signature().unwrap().len(), 65);
3509
3510        // None arms for address / shielded variants.
3511        assert!(sample_unshield_st().signature().is_none());
3512        assert!(sample_shielded_transfer_st().signature().is_none());
3513        assert!(sample_shielded_withdrawal_st().signature().is_none());
3514    }
3515
3516    // --- owner_id accessor for each arm.
3517    #[test]
3518    fn test_owner_id_accessor_for_other_variants() {
3519        assert_eq!(
3520            sample_data_contract_create_st().owner_id(),
3521            Some(Identifier::from([7u8; 32]))
3522        );
3523        assert_eq!(
3524            sample_data_contract_update_st().owner_id(),
3525            Some(Identifier::from([7u8; 32]))
3526        );
3527        assert_eq!(
3528            sample_batch_st_with_delete().owner_id(),
3529            Some(Identifier::from([8u8; 32]))
3530        );
3531        assert_eq!(
3532            sample_identity_update_st().owner_id(),
3533            Some(Identifier::from([5u8; 32]))
3534        );
3535        // These variants unconditionally return None.
3536        assert!(sample_unshield_st().owner_id().is_none());
3537        assert!(sample_shielded_transfer_st().owner_id().is_none());
3538        assert!(sample_shielded_withdrawal_st().owner_id().is_none());
3539    }
3540
3541    // --- user_fee_increase accessor — includes arms that return 0
3542    // unconditionally (shielded/masternode) vs the variants' stored value.
3543    #[test]
3544    fn test_user_fee_increase_for_newly_covered_variants() {
3545        assert_eq!(sample_data_contract_create_st().user_fee_increase(), 5);
3546        assert_eq!(sample_data_contract_update_st().user_fee_increase(), 9);
3547        assert_eq!(sample_batch_st_with_delete().user_fee_increase(), 2);
3548        assert_eq!(sample_identity_update_st().user_fee_increase(), 11);
3549        // Unconditionally 0 for shielded.
3550        assert_eq!(sample_shielded_transfer_st().user_fee_increase(), 0);
3551        assert_eq!(sample_shielded_withdrawal_st().user_fee_increase(), 0);
3552        assert_eq!(sample_unshield_st().user_fee_increase(), 0);
3553    }
3554
3555    // --- set_user_fee_increase for the no-op shielded arms and for the
3556    // transitions that actually do store the value.
3557    #[test]
3558    fn test_set_user_fee_increase_for_newly_covered_variants() {
3559        let mut st = sample_data_contract_create_st();
3560        st.set_user_fee_increase(42);
3561        assert_eq!(st.user_fee_increase(), 42);
3562
3563        let mut st = sample_data_contract_update_st();
3564        st.set_user_fee_increase(13);
3565        assert_eq!(st.user_fee_increase(), 13);
3566
3567        let mut st = sample_batch_st_with_delete();
3568        st.set_user_fee_increase(101);
3569        assert_eq!(st.user_fee_increase(), 101);
3570
3571        let mut st = sample_identity_update_st();
3572        st.set_user_fee_increase(77);
3573        assert_eq!(st.user_fee_increase(), 77);
3574
3575        // Shielded no-ops: value stays 0.
3576        let mut shielded = sample_shielded_transfer_st();
3577        shielded.set_user_fee_increase(99);
3578        assert_eq!(shielded.user_fee_increase(), 0);
3579
3580        let mut withdrawal = sample_shielded_withdrawal_st();
3581        withdrawal.set_user_fee_increase(99);
3582        assert_eq!(withdrawal.user_fee_increase(), 0);
3583
3584        let mut unshield = sample_unshield_st();
3585        unshield.set_user_fee_increase(99);
3586        assert_eq!(unshield.user_fee_increase(), 0);
3587    }
3588
3589    // --- set_signature: exercises the `true` arms we didn't test before
3590    // (DataContractCreate/Update/Batch/IdentityUpdate) and the `false` arms
3591    // (shielded transitions).
3592    #[test]
3593    fn test_set_signature_false_for_shielded_and_identity_create_topup() {
3594        // `false` arms: shield*, shielded*, unshield, address* (no-op, returns false).
3595        let mut st = sample_unshield_st();
3596        assert!(!st.set_signature(BinaryData::new(vec![0xAB; 65])));
3597        let mut st = sample_shielded_transfer_st();
3598        assert!(!st.set_signature(BinaryData::new(vec![0xAB; 65])));
3599        let mut st = sample_shielded_withdrawal_st();
3600        assert!(!st.set_signature(BinaryData::new(vec![0xAB; 65])));
3601    }
3602
3603    #[test]
3604    fn test_set_signature_true_for_newly_covered_variants() {
3605        let mut st = sample_data_contract_create_st();
3606        assert!(st.set_signature(BinaryData::new(vec![0x11; 65])));
3607        assert_eq!(st.signature().unwrap().as_slice(), &[0x11; 65]);
3608
3609        let mut st = sample_data_contract_update_st();
3610        assert!(st.set_signature(BinaryData::new(vec![0x22; 65])));
3611        assert_eq!(st.signature().unwrap().as_slice(), &[0x22; 65]);
3612
3613        let mut st = sample_batch_st_with_delete();
3614        assert!(st.set_signature(BinaryData::new(vec![0x33; 65])));
3615        assert_eq!(st.signature().unwrap().as_slice(), &[0x33; 65]);
3616
3617        let mut st = sample_identity_update_st();
3618        assert!(st.set_signature(BinaryData::new(vec![0x44; 65])));
3619        assert_eq!(st.signature().unwrap().as_slice(), &[0x44; 65]);
3620    }
3621
3622    // --- signature_public_key_id: identity-signed arms return Some, others
3623    // (shielded/identity-create/topup/address) return None.
3624    #[test]
3625    fn test_signature_public_key_id_returns_none_for_non_signed() {
3626        // IdentityCreate / IdentityTopUp / shielded / address variants are all
3627        // "not identity-signed" and return None.
3628        assert!(sample_identity_create_st()
3629            .signature_public_key_id()
3630            .is_none());
3631        assert!(sample_identity_top_up_st()
3632            .signature_public_key_id()
3633            .is_none());
3634        assert!(sample_unshield_st().signature_public_key_id().is_none());
3635        assert!(sample_shielded_transfer_st()
3636            .signature_public_key_id()
3637            .is_none());
3638        assert!(sample_shielded_withdrawal_st()
3639            .signature_public_key_id()
3640            .is_none());
3641    }
3642
3643    #[test]
3644    fn test_signature_public_key_id_for_signed_variants() {
3645        assert_eq!(
3646            sample_data_contract_create_st().signature_public_key_id(),
3647            Some(2)
3648        );
3649        assert_eq!(
3650            sample_data_contract_update_st().signature_public_key_id(),
3651            Some(6)
3652        );
3653        assert_eq!(
3654            sample_batch_st_with_delete().signature_public_key_id(),
3655            Some(7)
3656        );
3657        assert_eq!(
3658            sample_identity_update_st().signature_public_key_id(),
3659            Some(33)
3660        );
3661    }
3662
3663    // --- set_signature_public_key_id: no-op for IdentityCreate/TopUp and
3664    // shielded variants; updates for identity-signed variants. ---
3665    #[test]
3666    fn test_set_signature_public_key_id_noop_for_non_signed() {
3667        // These variants are not identity-signed; setter is a no-op in the
3668        // call_method_identity_signed! macro.
3669        let mut st = sample_identity_create_st();
3670        st.set_signature_public_key_id(100);
3671        assert_eq!(st.signature_public_key_id(), None);
3672
3673        let mut st = sample_identity_top_up_st();
3674        st.set_signature_public_key_id(100);
3675        assert_eq!(st.signature_public_key_id(), None);
3676
3677        let mut st = sample_unshield_st();
3678        st.set_signature_public_key_id(100);
3679        assert_eq!(st.signature_public_key_id(), None);
3680    }
3681
3682    #[test]
3683    fn test_set_signature_public_key_id_updates_for_signed_variants() {
3684        let mut st = sample_data_contract_create_st();
3685        st.set_signature_public_key_id(42);
3686        assert_eq!(st.signature_public_key_id(), Some(42));
3687
3688        let mut st = sample_batch_st_with_delete();
3689        st.set_signature_public_key_id(43);
3690        assert_eq!(st.signature_public_key_id(), Some(43));
3691
3692        let mut st = sample_identity_update_st();
3693        st.set_signature_public_key_id(44);
3694        assert_eq!(st.signature_public_key_id(), Some(44));
3695    }
3696
3697    // --- required_number_of_private_keys defaults to 1 for "signed" variants
3698    // and 0 for shielded ones.
3699    #[test]
3700    fn test_required_number_of_private_keys_various_variants() {
3701        assert_eq!(
3702            sample_data_contract_create_st().required_number_of_private_keys(),
3703            1
3704        );
3705        assert_eq!(
3706            sample_data_contract_update_st().required_number_of_private_keys(),
3707            1
3708        );
3709        assert_eq!(
3710            sample_batch_st_with_delete().required_number_of_private_keys(),
3711            1
3712        );
3713        assert_eq!(
3714            sample_identity_update_st().required_number_of_private_keys(),
3715            1
3716        );
3717        assert_eq!(
3718            sample_identity_create_st().required_number_of_private_keys(),
3719            1
3720        );
3721        // Shielded variants return 0 unconditionally.
3722        assert_eq!(
3723            sample_shielded_transfer_st().required_number_of_private_keys(),
3724            0
3725        );
3726        assert_eq!(
3727            sample_shielded_withdrawal_st().required_number_of_private_keys(),
3728            0
3729        );
3730        assert_eq!(sample_unshield_st().required_number_of_private_keys(), 0);
3731    }
3732
3733    // --- inputs(): None for all these variants (covers the big
3734    // wildcard/None arm in the match).
3735    #[test]
3736    fn test_inputs_none_for_many_variants() {
3737        assert!(sample_data_contract_create_st().inputs().is_none());
3738        assert!(sample_data_contract_update_st().inputs().is_none());
3739        assert!(sample_batch_st_with_delete().inputs().is_none());
3740        assert!(sample_identity_create_st().inputs().is_none());
3741        assert!(sample_identity_top_up_st().inputs().is_none());
3742        assert!(sample_identity_update_st().inputs().is_none());
3743        // Shielded variants also return None for inputs().
3744        assert!(sample_unshield_st().inputs().is_none());
3745        assert!(sample_shielded_transfer_st().inputs().is_none());
3746        assert!(sample_shielded_withdrawal_st().inputs().is_none());
3747    }
3748
3749    // --- optional_asset_lock_proof: None for everything that isn't
3750    // IdentityCreate / IdentityTopUp / ShieldFromAssetLock. The IdentityCreate
3751    // default contains the asset lock proof field, so this forwards to its
3752    // implementation.
3753    #[test]
3754    fn test_optional_asset_lock_proof_returns_none_for_wildcard_arms() {
3755        assert!(sample_data_contract_create_st()
3756            .optional_asset_lock_proof()
3757            .is_none());
3758        assert!(sample_data_contract_update_st()
3759            .optional_asset_lock_proof()
3760            .is_none());
3761        assert!(sample_batch_st_with_delete()
3762            .optional_asset_lock_proof()
3763            .is_none());
3764        assert!(sample_identity_update_st()
3765            .optional_asset_lock_proof()
3766            .is_none());
3767        assert!(sample_unshield_st().optional_asset_lock_proof().is_none());
3768        assert!(sample_shielded_transfer_st()
3769            .optional_asset_lock_proof()
3770            .is_none());
3771        assert!(sample_shielded_withdrawal_st()
3772            .optional_asset_lock_proof()
3773            .is_none());
3774    }
3775
3776    // --- required_asset_lock_balance_for_processing_start returns an
3777    // CorruptedCodeExecution error for non asset-lock variants. Exercise
3778    // additional arms beyond what the original transfer test covered.
3779    #[test]
3780    fn test_required_asset_lock_balance_errors_for_other_non_asset_lock_variants() {
3781        let platform_version = PlatformVersion::latest();
3782
3783        let cases: Vec<(&str, StateTransition)> = vec![
3784            ("DataContractCreate", sample_data_contract_create_st()),
3785            ("DataContractUpdate", sample_data_contract_update_st()),
3786            ("Batch", sample_batch_st_with_delete()),
3787            ("IdentityUpdate", sample_identity_update_st()),
3788            ("MasternodeVote", sample_masternode_vote_st()),
3789            ("Unshield", sample_unshield_st()),
3790            ("ShieldedTransfer", sample_shielded_transfer_st()),
3791            ("ShieldedWithdrawal", sample_shielded_withdrawal_st()),
3792        ];
3793
3794        for (label, st) in cases {
3795            let err = st
3796                .required_asset_lock_balance_for_processing_start(platform_version)
3797                .expect_err(&format!("expected error for {label}"));
3798            match err {
3799                ProtocolError::CorruptedCodeExecution(msg) => {
3800                    assert!(
3801                        msg.contains("is not an asset lock transaction"),
3802                        "unexpected error for {label}: {msg}"
3803                    );
3804                }
3805                other => panic!("expected CorruptedCodeExecution for {label}, got {other:?}"),
3806            }
3807        }
3808    }
3809
3810    // --- unique_identifiers: covers the call_method! dispatch for arms
3811    // beyond credit transfer. Each variant's `unique_identifiers`
3812    // implementation returns a non-empty vector; the individual identifier
3813    // strings may be empty for some variants whose IDs are encoded as empty
3814    // (this method simply shouldn't panic or short-circuit).
3815    #[test]
3816    fn test_unique_identifiers_non_empty_for_other_variants() {
3817        for st in [
3818            sample_data_contract_create_st(),
3819            sample_data_contract_update_st(),
3820            sample_batch_st_with_delete(),
3821            sample_identity_create_st(),
3822            sample_identity_top_up_st(),
3823            sample_identity_update_st(),
3824        ] {
3825            let ids = st.unique_identifiers();
3826            assert!(!ids.is_empty(), "unique_identifiers should not be empty");
3827        }
3828    }
3829
3830    // --- security_level_requirement returns None for identity-create/topup
3831    // and for every shielded/address variant. This hits the None arms in
3832    // call_getter_method_identity_signed!.
3833    #[test]
3834    fn test_security_level_requirement_returns_none_for_non_signed_variants() {
3835        let purpose = Purpose::AUTHENTICATION;
3836        assert!(sample_identity_create_st()
3837            .security_level_requirement(purpose)
3838            .is_none());
3839        assert!(sample_identity_top_up_st()
3840            .security_level_requirement(purpose)
3841            .is_none());
3842        assert!(sample_unshield_st()
3843            .security_level_requirement(purpose)
3844            .is_none());
3845        assert!(sample_shielded_transfer_st()
3846            .security_level_requirement(purpose)
3847            .is_none());
3848        assert!(sample_shielded_withdrawal_st()
3849            .security_level_requirement(purpose)
3850            .is_none());
3851    }
3852
3853    #[test]
3854    fn test_purpose_requirement_returns_none_for_non_signed_variants() {
3855        assert!(sample_identity_create_st().purpose_requirement().is_none());
3856        assert!(sample_identity_top_up_st().purpose_requirement().is_none());
3857        assert!(sample_unshield_st().purpose_requirement().is_none());
3858        assert!(sample_shielded_transfer_st()
3859            .purpose_requirement()
3860            .is_none());
3861        assert!(sample_shielded_withdrawal_st()
3862            .purpose_requirement()
3863            .is_none());
3864    }
3865
3866    // --- From impls: each From<Outer> → StateTransition uses `derive_more::From`.
3867    #[test]
3868    fn test_from_outer_data_contract_create_into_state_transition() {
3869        let outer: DataContractCreateTransition =
3870            DataContractCreateTransition::V0(DataContractCreateTransitionV0 {
3871                data_contract: sample_data_contract_in_serialization_format(),
3872                identity_nonce: 1,
3873                user_fee_increase: 0,
3874                signature_public_key_id: 0,
3875                signature: Default::default(),
3876            });
3877        let st: StateTransition = outer.into();
3878        assert!(matches!(st, StateTransition::DataContractCreate(_)));
3879    }
3880
3881    #[test]
3882    fn test_from_outer_data_contract_update_into_state_transition() {
3883        let outer: DataContractUpdateTransition =
3884            DataContractUpdateTransition::V0(DataContractUpdateTransitionV0 {
3885                identity_contract_nonce: 2,
3886                data_contract: sample_data_contract_in_serialization_format(),
3887                user_fee_increase: 0,
3888                signature_public_key_id: 0,
3889                signature: Default::default(),
3890            });
3891        let st: StateTransition = outer.into();
3892        assert!(matches!(st, StateTransition::DataContractUpdate(_)));
3893    }
3894
3895    #[test]
3896    fn test_from_outer_batch_into_state_transition() {
3897        let outer: BatchTransition = BatchTransition::V0(BatchTransitionV0::default());
3898        let st: StateTransition = outer.into();
3899        assert!(matches!(st, StateTransition::Batch(_)));
3900    }
3901
3902    #[test]
3903    fn test_from_outer_identity_create_into_state_transition() {
3904        let outer: IdentityCreateTransition =
3905            IdentityCreateTransition::V0(IdentityCreateTransitionV0::default());
3906        let st: StateTransition = outer.into();
3907        assert!(matches!(st, StateTransition::IdentityCreate(_)));
3908    }
3909
3910    #[test]
3911    fn test_from_outer_identity_update_into_state_transition() {
3912        let outer: IdentityUpdateTransition =
3913            IdentityUpdateTransition::V0(IdentityUpdateTransitionV0::default());
3914        let st: StateTransition = outer.into();
3915        assert!(matches!(st, StateTransition::IdentityUpdate(_)));
3916    }
3917
3918    // --- transaction_id + clone for additional variants — triggers the
3919    // serialize path for each arm.
3920    #[test]
3921    fn test_transaction_id_and_clone_for_identity_update() {
3922        let st = sample_identity_update_st();
3923        let id_a = st.transaction_id().expect("hash should succeed");
3924        let cloned = st.clone();
3925        let id_b = cloned.transaction_id().expect("hash should succeed");
3926        assert_eq!(id_a, id_b);
3927        assert_eq!(id_a.len(), 32);
3928    }
3929
3930    #[test]
3931    fn test_transaction_id_and_clone_for_data_contract_create() {
3932        let st = sample_data_contract_create_st();
3933        let id_a = st.transaction_id().expect("hash should succeed");
3934        let cloned = st.clone();
3935        let id_b = cloned.transaction_id().expect("hash should succeed");
3936        assert_eq!(id_a, id_b);
3937        assert_eq!(id_a.len(), 32);
3938    }
3939
3940    // --- serialize round-trip for variants beyond credit transfer. ---
3941    #[test]
3942    fn test_serialize_roundtrip_identity_update() {
3943        use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable};
3944        let original = sample_identity_update_st();
3945        let bytes =
3946            PlatformSerializable::serialize_to_bytes(&original).expect("serialize should succeed");
3947        let restored = StateTransition::deserialize_from_bytes_untrusted(&bytes)
3948            .expect("deserialize should succeed");
3949        assert_eq!(original, restored);
3950    }
3951
3952    #[test]
3953    fn test_serialize_roundtrip_data_contract_update() {
3954        use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable};
3955        let original = sample_data_contract_update_st();
3956        let bytes =
3957            PlatformSerializable::serialize_to_bytes(&original).expect("serialize should succeed");
3958        let restored = StateTransition::deserialize_from_bytes_untrusted(&bytes)
3959            .expect("deserialize should succeed");
3960        assert_eq!(original, restored);
3961    }
3962
3963    #[test]
3964    fn test_serialize_roundtrip_batch_empty() {
3965        use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable};
3966        let original = sample_batch_st_empty();
3967        let bytes =
3968            PlatformSerializable::serialize_to_bytes(&original).expect("serialize should succeed");
3969        let restored = StateTransition::deserialize_from_bytes_untrusted(&bytes)
3970            .expect("deserialize should succeed");
3971        assert_eq!(original, restored);
3972    }
3973
3974    // --- deserialize_from_bytes_in_version error path: craft bytes for a
3975    // variant whose `active_version_range()` starts at 11 or 12 and then
3976    // attempt to deserialize them with a PlatformVersion whose protocol
3977    // version is below that range. Exercises the
3978    // `StateTransitionIsNotActiveError` arm.
3979    // ---
3980    #[cfg(all(feature = "state-transitions", feature = "validation"))]
3981    #[test]
3982    fn test_deserialize_from_bytes_in_version_returns_not_active_error() {
3983        use crate::serialization::PlatformSerializable;
3984
3985        // ShieldedTransfer has active_version_range = 12..=LATEST_VERSION.
3986        let original = sample_shielded_transfer_st();
3987        let bytes =
3988            PlatformSerializable::serialize_to_bytes(&original).expect("serialize succeeds");
3989
3990        // Find a real PlatformVersion whose protocol_version is < 12 so the
3991        // range check rejects it. PlatformVersion::get(1) corresponds to
3992        // protocol version 1 which is guaranteed below any shielded range.
3993        let low_version = PlatformVersion::get(1).expect("platform version 1 exists");
3994        assert!(
3995            low_version.protocol_version < 12,
3996            "expected sub-12 version for this test, got {}",
3997            low_version.protocol_version
3998        );
3999
4000        let err = StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, low_version)
4001            .expect_err("expected StateTransitionIsNotActiveError for sub-12 protocol");
4002        match err {
4003            ProtocolError::StateTransitionError(
4004                crate::state_transition::errors::StateTransitionError::StateTransitionIsNotActiveError {
4005                    state_transition_type,
4006                    active_version_range,
4007                    current_protocol_version,
4008                },
4009            ) => {
4010                assert_eq!(state_transition_type, "ShieldedTransfer");
4011                assert_eq!(current_protocol_version, low_version.protocol_version);
4012                assert!(active_version_range.start() >= &12);
4013            }
4014            other => panic!("expected StateTransitionIsNotActiveError, got {other:?}"),
4015        }
4016    }
4017
4018    /// The exact decoder keeps the active version check: exact bytes of a transition that is
4019    /// not active yet are refused as not active, not as left over bytes.
4020    #[cfg(all(feature = "state-transitions", feature = "validation"))]
4021    #[test]
4022    fn should_refuse_an_inactive_transition_in_the_exact_in_version_decoder() {
4023        use crate::serialization::PlatformSerializable;
4024
4025        // ShieldedTransfer has active_version_range = 12..=LATEST_VERSION.
4026        let bytes = PlatformSerializable::serialize_to_bytes(&sample_shielded_transfer_st())
4027            .expect("serialize succeeds");
4028        let low_version = PlatformVersion::get(1).expect("platform version 1 exists");
4029
4030        assert!(matches!(
4031            StateTransition::deserialize_from_bytes_untrusted_exact_in_version(&bytes, low_version),
4032            Err(ProtocolError::StateTransitionError(
4033                crate::state_transition::errors::StateTransitionError::StateTransitionIsNotActiveError { .. }
4034            ))
4035        ));
4036    }
4037
4038    // A `ShieldFromAssetLock` is refused on its version byte at decode, before any proof work:
4039    // version 0 (whose bundle binds nothing) up to protocol version 13 only, version 1 (whose
4040    // bundle binds its kind and asset lock) from 14 only.
4041    #[cfg(all(feature = "state-transitions", feature = "validation"))]
4042    #[test]
4043    fn should_decode_each_shield_from_asset_lock_version_only_where_it_is_active() {
4044        use crate::serialization::PlatformSerializable;
4045        use crate::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1;
4046
4047        let v0 = sample_shield_from_asset_lock_st();
4048        let StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0(body)) = &v0
4049        else {
4050            panic!("expected a version 0 shield from asset lock");
4051        };
4052        let v1 = StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V1(
4053            ShieldFromAssetLockTransitionV1 {
4054                asset_lock_proof: body.asset_lock_proof.clone(),
4055                actions: body.actions.clone(),
4056                value_balance: body.value_balance,
4057                anchor: body.anchor,
4058                proof: body.proof.clone(),
4059                binding_signature: body.binding_signature,
4060                surplus_output: body.surplus_output,
4061                signature: body.signature.clone(),
4062            },
4063        ));
4064        assert_eq!(v0.active_version_range(), 12..=13);
4065        assert_eq!(v1.active_version_range(), 14..=LATEST_VERSION);
4066
4067        let decodes_at = |transition: &StateTransition, protocol_version: u32| {
4068            let bytes =
4069                PlatformSerializable::serialize_to_bytes(transition).expect("serialize succeeds");
4070            let platform_version =
4071                PlatformVersion::get(protocol_version).expect("known protocol version");
4072            match StateTransition::deserialize_from_bytes_untrusted_in_version(
4073                &bytes,
4074                platform_version,
4075            ) {
4076                Ok(decoded) => {
4077                    assert_eq!(&decoded, transition);
4078                    true
4079                }
4080                Err(ProtocolError::StateTransitionError(
4081                    crate::state_transition::errors::StateTransitionError::StateTransitionIsNotActiveError {
4082                        current_protocol_version,
4083                        ..
4084                    },
4085                )) => {
4086                    assert_eq!(current_protocol_version, protocol_version);
4087                    false
4088                }
4089                Err(other) => panic!("unexpected decode error: {other:?}"),
4090            }
4091        };
4092
4093        for protocol_version in [12, 13] {
4094            assert!(decodes_at(&v0, protocol_version));
4095            assert!(!decodes_at(&v1, protocol_version));
4096        }
4097        assert!(
4098            !decodes_at(&v0, 14),
4099            "protocol version 14 must refuse version 0"
4100        );
4101        assert!(decodes_at(&v1, 14));
4102    }
4103
4104    // A version 1 data contract create carries contract groups, which only exist from
4105    // protocol version 14. Below that a node must reject it rather than create the
4106    // contract and drop the group data.
4107    #[test]
4108    fn test_data_contract_create_v1_is_not_active_before_protocol_version_14() {
4109        use crate::serialization::PlatformSerializable;
4110
4111        let original = sample_data_contract_create_v1_st();
4112        assert_eq!(original.active_version_range(), 14..=LATEST_VERSION);
4113
4114        let bytes =
4115            PlatformSerializable::serialize_to_bytes(&original).expect("serialize succeeds");
4116
4117        let version_13 = PlatformVersion::get(13).expect("platform version 13 exists");
4118        let err = StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, version_13)
4119            .expect_err("expected StateTransitionIsNotActiveError at protocol version 13");
4120        match err {
4121            ProtocolError::StateTransitionError(
4122                crate::state_transition::errors::StateTransitionError::StateTransitionIsNotActiveError {
4123                    active_version_range,
4124                    current_protocol_version,
4125                    ..
4126                },
4127            ) => {
4128                assert_eq!(current_protocol_version, 13);
4129                assert_eq!(*active_version_range.start(), 14);
4130            }
4131            other => panic!("expected StateTransitionIsNotActiveError, got {other:?}"),
4132        }
4133
4134        let version_14 = PlatformVersion::get(14).expect("platform version 14 exists");
4135        StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, version_14)
4136            .expect("a version 1 create is active at protocol version 14");
4137    }
4138
4139    #[test]
4140    fn should_gate_identity_transitions_carrying_a_contract_group_bound_key_to_protocol_version_14()
4141    {
4142        use crate::identity::contract_bounds::ContractBounds;
4143        use crate::identity::{KeyType, Purpose, SecurityLevel};
4144        use crate::serialization::PlatformSerializable;
4145        use crate::state_transition::errors::StateTransitionError;
4146        use crate::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0;
4147        use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation;
4148
4149        let key = |contract_bounds| {
4150            IdentityPublicKeyInCreation::V0(IdentityPublicKeyInCreationV0 {
4151                id: 1,
4152                key_type: KeyType::ECDSA_SECP256K1,
4153                purpose: Purpose::AUTHENTICATION,
4154                security_level: SecurityLevel::HIGH,
4155                contract_bounds,
4156                read_only: false,
4157                data: BinaryData::new(vec![2u8; 33]),
4158                signature: BinaryData::default(),
4159            })
4160        };
4161        let contract_bound = Some(ContractBounds::SingleContract {
4162            id: Identifier::from([9u8; 32]),
4163        });
4164        let group_bound = Some(ContractBounds::ContractGroup {
4165            id: Identifier::from([9u8; 32]),
4166        });
4167        let create = |bounds| {
4168            StateTransition::IdentityCreate(IdentityCreateTransition::V0(
4169                IdentityCreateTransitionV0 {
4170                    identity_id: Identifier::from([3u8; 32]),
4171                    public_keys: vec![key(bounds)],
4172                    ..Default::default()
4173                },
4174            ))
4175        };
4176        let update = |bounds| {
4177            let StateTransition::IdentityUpdate(IdentityUpdateTransition::V0(mut v0)) =
4178                sample_identity_update_st()
4179            else {
4180                panic!("expected a version 0 identity update");
4181            };
4182            v0.add_public_keys = vec![key(bounds)];
4183            StateTransition::IdentityUpdate(IdentityUpdateTransition::V0(v0))
4184        };
4185
4186        assert_eq!(
4187            create(contract_bound.clone()).active_version_range(),
4188            ALL_VERSIONS
4189        );
4190        assert_eq!(
4191            create(group_bound.clone()).active_version_range(),
4192            14..=LATEST_VERSION
4193        );
4194        assert_eq!(update(contract_bound).active_version_range(), ALL_VERSIONS);
4195        assert_eq!(
4196            update(group_bound.clone()).active_version_range(),
4197            14..=LATEST_VERSION
4198        );
4199
4200        let bytes = PlatformSerializable::serialize_to_bytes(&update(group_bound))
4201            .expect("serialize succeeds");
4202        let version_13 = PlatformVersion::get(13).expect("platform version 13 exists");
4203        let err = StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, version_13)
4204            .expect_err("a contract group bound key is not active at protocol version 13");
4205        assert!(
4206            matches!(
4207                err,
4208                ProtocolError::StateTransitionError(
4209                    StateTransitionError::StateTransitionIsNotActiveError { .. }
4210                )
4211            ),
4212            "{err:?}"
4213        );
4214        let version_14 = PlatformVersion::get(14).expect("platform version 14 exists");
4215        StateTransition::deserialize_from_bytes_untrusted_in_version(&bytes, version_14)
4216            .expect("a contract group bound key is active at protocol version 14");
4217    }
4218
4219    // -----------------------------------------------------------------------
4220    // Additional coverage: variants not yet exercised.
4221    //
4222    // The tests below target:
4223    //   * IdentityCreditWithdrawal::V1 (previously only V0 was covered).
4224    //   * IdentityCreditTransferToAddresses (its own top-level arm).
4225    //   * AddressFundingFromAssetLock (identity-signed + asset-lock arm).
4226    //   * AddressCreditWithdrawal (not identity-signed, address-funds arm).
4227    //   * ShieldFromAssetLock (asset-lock, non-identity-signed).
4228    //   * Batch with a token transition (nested enum, previously only Delete).
4229    // -----------------------------------------------------------------------
4230
4231    use crate::state_transition::identity_credit_transfer_to_addresses_transition::v0::IdentityCreditTransferToAddressesTransitionV0;
4232    use crate::state_transition::identity_credit_transfer_to_addresses_transition::IdentityCreditTransferToAddressesTransition;
4233    use crate::state_transition::identity_credit_withdrawal_transition::v1::IdentityCreditWithdrawalTransitionV1;
4234    use crate::withdrawal::Pooling as WithdrawalPooling;
4235
4236    fn sample_withdrawal_v1_st() -> StateTransition {
4237        let v1 = IdentityCreditWithdrawalTransitionV1 {
4238            identity_id: Identifier::from([12u8; 32]),
4239            amount: 777,
4240            core_fee_per_byte: 2,
4241            pooling: WithdrawalPooling::Standard,
4242            output_script: None,
4243            nonce: 9,
4244            user_fee_increase: 4,
4245            signature_public_key_id: 21,
4246            signature: BinaryData::new(vec![0x12; 65]),
4247        };
4248        StateTransition::IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition::V1(v1))
4249    }
4250
4251    fn sample_credit_transfer_to_addresses_st() -> StateTransition {
4252        let v0 = IdentityCreditTransferToAddressesTransitionV0 {
4253            identity_id: Identifier::from([13u8; 32]),
4254            ..Default::default()
4255        };
4256        StateTransition::IdentityCreditTransferToAddresses(
4257            IdentityCreditTransferToAddressesTransition::V0(v0),
4258        )
4259    }
4260
4261    fn sample_address_credit_withdrawal_st() -> StateTransition {
4262        use crate::state_transition::address_credit_withdrawal_transition::v0::AddressCreditWithdrawalTransitionV0;
4263        StateTransition::AddressCreditWithdrawal(AddressCreditWithdrawalTransition::V0(
4264            AddressCreditWithdrawalTransitionV0::default(),
4265        ))
4266    }
4267
4268    fn sample_shield_from_asset_lock_st() -> StateTransition {
4269        use crate::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0;
4270        StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0(
4271            ShieldFromAssetLockTransitionV0 {
4272                asset_lock_proof: Default::default(),
4273                actions: vec![],
4274                value_balance: 100,
4275                anchor: [0u8; 32],
4276                proof: vec![],
4277                binding_signature: [0u8; 64],
4278                surplus_output: None,
4279                signature: BinaryData::new(vec![0x55; 65]),
4280            },
4281        ))
4282    }
4283
4284    // ---------- IdentityCreditWithdrawal V1 accessors ----------
4285
4286    #[test]
4287    fn test_withdrawal_v1_name_and_type() {
4288        let st = sample_withdrawal_v1_st();
4289        assert_eq!(st.name(), "IdentityCreditWithdrawal");
4290        assert_eq!(
4291            st.state_transition_type(),
4292            StateTransitionType::IdentityCreditWithdrawal
4293        );
4294    }
4295
4296    #[test]
4297    fn test_withdrawal_v1_is_identity_signed_true() {
4298        assert!(sample_withdrawal_v1_st().is_identity_signed());
4299    }
4300
4301    #[test]
4302    fn test_withdrawal_v1_signature_and_owner_and_key_id() {
4303        let st = sample_withdrawal_v1_st();
4304        // signature accessor -> Some
4305        let sig = st.signature().expect("V1 withdrawal has a signature");
4306        assert_eq!(sig.as_slice(), &[0x12; 65]);
4307        // owner_id delegates to identity_id
4308        assert_eq!(st.owner_id(), Some(Identifier::from([12u8; 32])));
4309        assert_eq!(st.signature_public_key_id(), Some(21));
4310        assert_eq!(st.user_fee_increase(), 4);
4311    }
4312
4313    #[test]
4314    fn test_withdrawal_v1_set_signature_and_fee_and_key_id() {
4315        let mut st = sample_withdrawal_v1_st();
4316        assert!(st.set_signature(BinaryData::new(vec![0x99; 65])));
4317        assert_eq!(st.signature().unwrap().as_slice(), &[0x99; 65]);
4318
4319        st.set_user_fee_increase(33);
4320        assert_eq!(st.user_fee_increase(), 33);
4321
4322        st.set_signature_public_key_id(64);
4323        assert_eq!(st.signature_public_key_id(), Some(64));
4324    }
4325
4326    #[test]
4327    fn test_withdrawal_v1_serialize_roundtrip_via_state_transition() {
4328        use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable};
4329        let original = sample_withdrawal_v1_st();
4330        let bytes = PlatformSerializable::serialize_to_bytes(&original).expect("serialize ok");
4331        let restored =
4332            StateTransition::deserialize_from_bytes_untrusted(&bytes).expect("deserialize ok");
4333        assert_eq!(original, restored);
4334        // The restored variant must still be V1, not V0 — exercises the
4335        // feature-version dispatch in deserialize.
4336        match restored {
4337            StateTransition::IdentityCreditWithdrawal(IdentityCreditWithdrawalTransition::V1(
4338                _,
4339            )) => {}
4340            other => panic!("expected V1 inner variant, got: {:?}", other),
4341        }
4342    }
4343
4344    #[test]
4345    fn test_withdrawal_v1_transaction_id_differs_from_v0() {
4346        // V0 and V1 carry different serialized forms → distinct transaction ids.
4347        let v0 = sample_withdrawal_st();
4348        let v1 = sample_withdrawal_v1_st();
4349        let id_v0 = v0.transaction_id().expect("v0 hash");
4350        let id_v1 = v1.transaction_id().expect("v1 hash");
4351        assert_ne!(id_v0, id_v1);
4352    }
4353
4354    #[test]
4355    fn test_withdrawal_v1_required_asset_lock_balance_errors() {
4356        let err = sample_withdrawal_v1_st()
4357            .required_asset_lock_balance_for_processing_start(PlatformVersion::latest())
4358            .expect_err("withdrawal is not an asset lock ST");
4359        matches!(err, ProtocolError::CorruptedCodeExecution(_));
4360    }
4361
4362    // ---------- IdentityCreditTransferToAddresses ----------
4363
4364    #[test]
4365    fn test_credit_transfer_to_addresses_name_and_type() {
4366        let st = sample_credit_transfer_to_addresses_st();
4367        assert_eq!(st.name(), "IdentityCreditTransferToAddresses");
4368        assert_eq!(
4369            st.state_transition_type(),
4370            StateTransitionType::IdentityCreditTransferToAddresses
4371        );
4372    }
4373
4374    #[test]
4375    fn test_credit_transfer_to_addresses_signature_some_and_owner_some() {
4376        let st = sample_credit_transfer_to_addresses_st();
4377        assert!(st.signature().is_some(), "has a signature field");
4378        assert_eq!(st.owner_id(), Some(Identifier::from([13u8; 32])));
4379    }
4380
4381    #[test]
4382    fn test_credit_transfer_to_addresses_is_identity_signed_true() {
4383        // Not in the "not identity signed" list → should be true.
4384        assert!(sample_credit_transfer_to_addresses_st().is_identity_signed());
4385    }
4386
4387    #[test]
4388    fn test_credit_transfer_to_addresses_inputs_none_active_range_11_latest() {
4389        let st = sample_credit_transfer_to_addresses_st();
4390        assert!(st.inputs().is_none());
4391        // Per mod.rs table: this variant is in the 11..=LATEST_VERSION group.
4392        let range = st.active_version_range();
4393        assert_eq!(*range.start(), 11);
4394        assert_eq!(*range.end(), LATEST_VERSION);
4395    }
4396
4397    #[test]
4398    fn test_credit_transfer_to_addresses_set_signature_returns_true() {
4399        let mut st = sample_credit_transfer_to_addresses_st();
4400        let ok = st.set_signature(BinaryData::new(vec![0x77; 65]));
4401        assert!(ok);
4402        assert_eq!(st.signature().unwrap().as_slice(), &[0x77; 65]);
4403    }
4404
4405    #[test]
4406    fn test_credit_transfer_to_addresses_from_outer_enum() {
4407        let outer = IdentityCreditTransferToAddressesTransition::V0(
4408            IdentityCreditTransferToAddressesTransitionV0::default(),
4409        );
4410        let st: StateTransition = outer.into();
4411        assert!(matches!(
4412            st,
4413            StateTransition::IdentityCreditTransferToAddresses(_)
4414        ));
4415    }
4416
4417    #[test]
4418    fn test_credit_transfer_to_addresses_user_fee_increase_setter() {
4419        let mut st = sample_credit_transfer_to_addresses_st();
4420        st.set_user_fee_increase(55);
4421        assert_eq!(st.user_fee_increase(), 55);
4422    }
4423
4424    // ---------- AddressCreditWithdrawal ----------
4425
4426    #[test]
4427    fn test_address_credit_withdrawal_name_type_and_accessors() {
4428        let st = sample_address_credit_withdrawal_st();
4429        assert_eq!(st.name(), "AddressCreditWithdrawal");
4430        assert_eq!(
4431            st.state_transition_type(),
4432            StateTransitionType::AddressCreditWithdrawal
4433        );
4434        // signature is None for AddressCreditWithdrawal (see mod.rs arm).
4435        assert!(st.signature().is_none());
4436        // owner_id is None for every address-* variant.
4437        assert!(st.owner_id().is_none());
4438        // inputs → Some (delegated to inner struct's inputs map, may be empty).
4439        assert!(st.inputs().is_some());
4440    }
4441
4442    #[test]
4443    fn test_address_credit_withdrawal_set_signature_returns_false() {
4444        let mut st = sample_address_credit_withdrawal_st();
4445        assert!(!st.set_signature(BinaryData::new(vec![0xAB; 65])));
4446    }
4447
4448    #[test]
4449    fn test_address_credit_withdrawal_is_identity_signed_true() {
4450        // Per mod.rs: `is_identity_signed` is !matches!(identity_create/topup/shield*/unshield/shielded*)
4451        // so address-* variants return true — even though signature() returns None.
4452        assert!(sample_address_credit_withdrawal_st().is_identity_signed());
4453    }
4454
4455    #[test]
4456    fn test_address_credit_withdrawal_active_range_is_11_latest() {
4457        let range = sample_address_credit_withdrawal_st().active_version_range();
4458        assert_eq!(*range.start(), 11);
4459        assert_eq!(*range.end(), LATEST_VERSION);
4460    }
4461
4462    // ---------- ShieldFromAssetLock ----------
4463
4464    #[test]
4465    fn test_shield_from_asset_lock_name_type_and_accessors() {
4466        let st = sample_shield_from_asset_lock_st();
4467        assert_eq!(st.name(), "ShieldFromAssetLock");
4468        assert_eq!(
4469            st.state_transition_type(),
4470            StateTransitionType::ShieldFromAssetLock
4471        );
4472        // signature IS present on ShieldFromAssetLock — Some arm.
4473        let sig = st
4474            .signature()
4475            .expect("shield-from-asset-lock has signature");
4476        assert_eq!(sig.as_slice(), &[0x55; 65]);
4477        // owner_id is always None for shielded-* arms.
4478        assert!(st.owner_id().is_none());
4479    }
4480
4481    #[test]
4482    fn test_shield_from_asset_lock_is_not_identity_signed() {
4483        assert!(!sample_shield_from_asset_lock_st().is_identity_signed());
4484    }
4485
4486    #[test]
4487    fn test_shield_from_asset_lock_optional_asset_lock_proof_some() {
4488        // Critical: this is one of the THREE arms where optional_asset_lock_proof
4489        // actually forwards to Some(_). Other Some arms are covered by
4490        // IdentityCreate and IdentityTopUp which have default asset lock proof.
4491        let st = sample_shield_from_asset_lock_st();
4492        assert!(st.optional_asset_lock_proof().is_some());
4493    }
4494
4495    #[test]
4496    fn test_shield_from_asset_lock_user_fee_increase_is_zero_and_setter_noop() {
4497        let mut st = sample_shield_from_asset_lock_st();
4498        assert_eq!(st.user_fee_increase(), 0);
4499        st.set_user_fee_increase(123);
4500        // Set is a no-op per mod.rs table.
4501        assert_eq!(st.user_fee_increase(), 0);
4502    }
4503
4504    #[test]
4505    fn test_shield_from_asset_lock_set_signature_returns_true() {
4506        let mut st = sample_shield_from_asset_lock_st();
4507        assert!(st.set_signature(BinaryData::new(vec![0x44; 65])));
4508        assert_eq!(st.signature().unwrap().as_slice(), &[0x44; 65]);
4509    }
4510
4511    #[test]
4512    fn test_shield_from_asset_lock_required_asset_lock_balance_succeeds() {
4513        // This is the only arm besides IdentityCreate/TopUp/AddressFundingFromAssetLock
4514        // that returns Ok from required_asset_lock_balance_for_processing_start.
4515        let st = sample_shield_from_asset_lock_st();
4516        let result = st.required_asset_lock_balance_for_processing_start(PlatformVersion::latest());
4517        assert!(
4518            result.is_ok(),
4519            "ShieldFromAssetLock should return Ok, got {:?}",
4520            result
4521        );
4522    }
4523
4524    #[test]
4525    fn test_shield_from_asset_lock_version_0_active_range_12_to_13() {
4526        // Version 0's bundle binds nothing; protocol version 14 admits only version 1.
4527        let range = sample_shield_from_asset_lock_st().active_version_range();
4528        assert_eq!(range, 12..=13);
4529    }
4530
4531    // ---------- Batch with Token transition exercises TokenTransfer arm
4532    //            in the name() nested match. ----------
4533
4534    #[test]
4535    fn test_batch_with_token_transfer_name_contains_token_transfer() {
4536        use crate::state_transition::batch_transition::batched_transition::token_transition::TokenTransition as TT;
4537        use crate::state_transition::batch_transition::batched_transition::BatchedTransition;
4538        use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0;
4539        use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition;
4540        use crate::state_transition::batch_transition::token_transfer_transition::v0::TokenTransferTransitionV0;
4541        use crate::state_transition::batch_transition::token_transfer_transition::TokenTransferTransition;
4542        use crate::state_transition::batch_transition::BatchTransitionV1;
4543
4544        let base = TokenBaseTransition::V0(TokenBaseTransitionV0 {
4545            identity_contract_nonce: 1,
4546            token_contract_position: 0,
4547            data_contract_id: Identifier::from([1u8; 32]),
4548            token_id: Identifier::from([2u8; 32]),
4549            using_group_info: None,
4550        });
4551        let token_transfer = TokenTransferTransition::V0(TokenTransferTransitionV0 {
4552            base,
4553            amount: 100,
4554            recipient_id: Identifier::from([3u8; 32]),
4555            public_note: None,
4556            shared_encrypted_note: None,
4557            private_encrypted_note: None,
4558        });
4559
4560        // BatchTransitionV1 is used for tokens. Build a single-token batch.
4561        let batch = BatchTransition::V1(BatchTransitionV1 {
4562            owner_id: Identifier::from([9u8; 32]),
4563            transitions: vec![BatchedTransition::Token(TT::Transfer(token_transfer))],
4564            user_fee_increase: 0,
4565            signature_public_key_id: 0,
4566            signature: BinaryData::new(vec![0u8; 65]),
4567        });
4568        let st = StateTransition::Batch(batch);
4569
4570        assert_eq!(st.name(), "DocumentsBatch([TokenTransfer])");
4571    }
4572
4573    // -----------------------------------------------------------------------
4574    // Cross-variant consistency: transaction_id is a 32-byte blake3/sha256 hash
4575    // of the serialized form. Make sure it's stable across clones for the newly
4576    // covered variants too.
4577    // -----------------------------------------------------------------------
4578
4579    #[test]
4580    fn test_transaction_id_length_32_for_new_variants() {
4581        for st in [
4582            sample_withdrawal_v1_st(),
4583            sample_credit_transfer_to_addresses_st(),
4584            sample_shield_from_asset_lock_st(),
4585        ] {
4586            let id = st.transaction_id().expect("hash");
4587            assert_eq!(id.len(), 32);
4588        }
4589    }
4590
4591    // -----------------------------------------------------------------------
4592    // Clone-and-equality coverage for newly added variants (PartialEq via
4593    // derived impl, exercises the top-level enum's PartialEq arms).
4594    // -----------------------------------------------------------------------
4595
4596    #[test]
4597    fn test_clone_eq_for_new_variants() {
4598        let cases = [
4599            sample_withdrawal_v1_st(),
4600            sample_credit_transfer_to_addresses_st(),
4601            sample_address_credit_withdrawal_st(),
4602            sample_shield_from_asset_lock_st(),
4603        ];
4604        for st in cases {
4605            let cloned = st.clone();
4606            assert_eq!(st, cloned, "clone must be equal for {}", st.name());
4607        }
4608    }
4609
4610    // -----------------------------------------------------------------------
4611    // unique_identifiers() for address-* variants: the implementation
4612    // dispatches via call_method! and each variant returns a non-empty Vec.
4613    // -----------------------------------------------------------------------
4614
4615    #[test]
4616    fn test_unique_identifiers_for_address_and_shielded_variants() {
4617        // The address variants compute identifiers from their `inputs` map.
4618        // With a default (empty inputs) transition, unique_identifiers is empty
4619        // — that's fine, but we still want to exercise the call_method!
4620        // dispatch for these arms without panicking.
4621        for st in [
4622            sample_address_credit_withdrawal_st(),
4623            sample_shield_from_asset_lock_st(),
4624            sample_credit_transfer_to_addresses_st(),
4625            sample_withdrawal_v1_st(),
4626        ] {
4627            // Just calling unique_identifiers exercises the match arm; the
4628            // result may be empty for default-constructed inputs-based
4629            // variants, non-empty for identity-based ones.
4630            let _ids = st.unique_identifiers();
4631        }
4632        // For the identity-based variants the result IS non-empty.
4633        assert!(!sample_withdrawal_v1_st().unique_identifiers().is_empty());
4634        assert!(!sample_credit_transfer_to_addresses_st()
4635            .unique_identifiers()
4636            .is_empty());
4637    }
4638
4639    // -----------------------------------------------------------------------
4640    // sign_with_core_signer byte-parity test
4641    //
4642    // Proves that `StateTransition::sign_with_core_signer` produces a
4643    // byte-identical signature to the legacy `sign_by_private_key` ECDSA path
4644    // when both are driven by the same underlying secret. This is the on-wire
4645    // contract the Swift / external-signer flow depends on: changing the
4646    // digest pre-image or the recoverable-compact encoding would silently
4647    // break asset-lock verification on testnet/mainnet, so we pin both shapes
4648    // here.
4649    // -----------------------------------------------------------------------
4650    #[cfg(all(
4651        feature = "state-transition-signing",
4652        feature = "core_key_wallet",
4653        feature = "bls-signatures"
4654    ))]
4655    #[tokio::test]
4656    async fn sign_with_core_signer_matches_sign_by_private_key_byte_for_byte() {
4657        use async_trait::async_trait;
4658        use dashcore::secp256k1::{
4659            ecdsa, rand::rngs::OsRng, Message, PublicKey, Secp256k1, SecretKey,
4660        };
4661        use key_wallet::bip32::{DerivationPath, ExtendedPubKey};
4662        use key_wallet::signer::{ExtendedPubKeySigner, Signer as KwSigner, SignerMethod};
4663
4664        /// Fixed-key in-memory signer used only by this test. Mirrors how a
4665        /// real KeychainSigner would behave: derive once, sign atomically,
4666        /// return non-recoverable `(Signature, PublicKey)`. The path is
4667        /// ignored — the wrapper holds exactly one key.
4668        #[derive(Debug)]
4669        struct FixedKeySigner {
4670            secret: SecretKey,
4671            public: PublicKey,
4672        }
4673
4674        #[async_trait]
4675        impl KwSigner for FixedKeySigner {
4676            type Error = String;
4677
4678            fn supported_methods(&self) -> &[SignerMethod] {
4679                &[SignerMethod::Digest]
4680            }
4681
4682            async fn sign_ecdsa(
4683                &self,
4684                _path: &DerivationPath,
4685                sighash: [u8; 32],
4686            ) -> Result<(ecdsa::Signature, PublicKey), Self::Error> {
4687                let secp = Secp256k1::new();
4688                let msg = Message::from_digest(sighash);
4689                let sig = secp.sign_ecdsa(&msg, &self.secret);
4690                Ok((sig, self.public))
4691            }
4692
4693            async fn public_key(&self, _path: &DerivationPath) -> Result<PublicKey, Self::Error> {
4694                Ok(self.public)
4695            }
4696        }
4697
4698        #[async_trait]
4699        impl ExtendedPubKeySigner for FixedKeySigner {
4700            async fn extended_public_key(
4701                &self,
4702                _path: &DerivationPath,
4703            ) -> Result<ExtendedPubKey, Self::Error> {
4704                Err("FixedKeySigner does not derive extended public keys".to_string())
4705            }
4706        }
4707
4708        // Generate a single random key. Using the same key on both sides is
4709        // load-bearing: the legacy path signs raw bytes, the signer path
4710        // derives + signs inside the trust boundary. If the digest pre-image
4711        // or compact-encoding differs, the bytes will diverge.
4712        let secp = Secp256k1::new();
4713        let (secret_key, public_key) = secp.generate_keypair(&mut OsRng);
4714        let private_key_bytes = secret_key.secret_bytes();
4715
4716        let signer = FixedKeySigner {
4717            secret: secret_key,
4718            public: public_key,
4719        };
4720        let path = DerivationPath::default();
4721
4722        // Use a sample state transition that exercises signable_bytes() —
4723        // any signable ST works since we're only comparing the signature
4724        // bytes the two paths produce over the SAME `signable_bytes()`.
4725        let mut st_legacy = sample_transfer_st();
4726        let mut st_signer = sample_transfer_st();
4727
4728        // Sanity: both copies must have identical signable_bytes before signing.
4729        assert_eq!(
4730            st_legacy.signable_bytes().expect("legacy signable_bytes"),
4731            st_signer.signable_bytes().expect("signer signable_bytes"),
4732            "signable_bytes pre-image must match across copies"
4733        );
4734
4735        // Legacy path: raw &[u8] private key → 65-byte recoverable compact.
4736        // BLS is only used by `sign_by_private_key` when key_type is BLS12_381 —
4737        // for the ECDSA path it's unused, but the function signature requires
4738        // it, so we pass the NativeBlsModule that's already in the workspace.
4739        let bls = crate::bls::native_bls::NativeBlsModule;
4740        st_legacy
4741            .sign_by_private_key(&private_key_bytes, KeyType::ECDSA_HASH160, &bls)
4742            .expect("sign_by_private_key");
4743
4744        // New signer-driven path: digest → external signer → recovered →
4745        // 65-byte recoverable compact. Byte-identical to the legacy result.
4746        st_signer
4747            .sign_with_core_signer(&path, &signer)
4748            .await
4749            .expect("sign_with_core_signer");
4750
4751        let sig_legacy = st_legacy.signature().expect("legacy signature set");
4752        let sig_signer = st_signer.signature().expect("signer signature set");
4753
4754        assert_eq!(
4755            sig_legacy.as_slice().len(),
4756            65,
4757            "legacy ECDSA signature must be 65 bytes (recoverable compact)"
4758        );
4759        assert_eq!(
4760            sig_signer.as_slice().len(),
4761            65,
4762            "signer ECDSA signature must be 65 bytes (recoverable compact)"
4763        );
4764        assert_eq!(
4765            sig_legacy.as_slice(),
4766            sig_signer.as_slice(),
4767            "sign_with_core_signer must produce byte-identical output to sign_by_private_key"
4768        );
4769    }
4770}