From 82fd8fe4d8d289720b84d87d63a75861a48166b8 Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 26 Nov 2024 12:56:23 +0000 Subject: [PATCH 1/4] tests: Add demonstration of hash structure of notarized transaction v2 --- .../src/manifest/manifest_instructions.rs | 9 + .../src/model/any_transaction.rs | 376 +++++++++++++++--- .../src/model/v2/intent_signatures_v2.rs | 4 + .../src/model/v2/notarized_transaction_v2.rs | 4 + 4 files changed, 328 insertions(+), 65 deletions(-) diff --git a/radix-transactions/src/manifest/manifest_instructions.rs b/radix-transactions/src/manifest/manifest_instructions.rs index ca23e1d432..0e319bf14f 100644 --- a/radix-transactions/src/manifest/manifest_instructions.rs +++ b/radix-transactions/src/manifest/manifest_instructions.rs @@ -1374,6 +1374,15 @@ pub struct YieldToChild { pub args: ManifestValue, } +impl YieldToChild { + pub fn empty(index: u32) -> Self { + Self { + child_index: ManifestNamedIntentIndex(index), + args: ManifestValue::unit(), + } + } +} + impl ManifestInstruction for YieldToChild { const IDENT: &'static str = "YIELD_TO_CHILD"; const ID: u8 = INSTRUCTION_YIELD_TO_CHILD_DISCRIMINATOR; diff --git a/radix-transactions/src/model/any_transaction.rs b/radix-transactions/src/model/any_transaction.rs index 18bdff251d..15e68e73d4 100644 --- a/radix-transactions/src/model/any_transaction.rs +++ b/radix-transactions/src/model/any_transaction.rs @@ -110,6 +110,7 @@ mod tests { use crate::manifest::e2e::tests::print_blob; use crate::model::*; + #[deprecated = "Should only be used by transaction v1, because it's less flexible than hash_encoded_sbor_value_body"] fn hash_encoded_sbor_value(value: T) -> Hash { // Ignore the version byte hash(&manifest_encode(&value).unwrap()[1..]) @@ -120,9 +121,27 @@ mod tests { hash(&manifest_encode(&value).unwrap()[2..]) } + fn hash_contatenated_hashes>(hashes: impl IntoIterator) -> Hash { + let concatenated_hashes: Vec = hashes + .into_iter() + .map(|h| Into::::into(h).0) + .flatten() + .collect(); + hash(concatenated_hashes) + } + + fn hash_from_partial_prepare(value: &impl TransactionPartialPrepare) -> Hash { + value + .prepare_partial(PreparationSettings::latest_ref()) + .unwrap() + .get_summary() + .hash + } + /// This test demonstrates how the hashes and payloads are constructed in a valid user transaction. /// It also provides an example payload which can be used in other implementations. #[test] + #[allow(deprecated)] // Transaction V1 is allowed to use deprecated hashing pub fn v1_user_transaction_structure() { let network = NetworkDefinition::simulator(); let preparation_settings = PreparationSettings::babylon(); @@ -347,40 +366,250 @@ mod tests { pub fn v2_notarized_transaction_structure() { let network = NetworkDefinition::simulator(); - // TODO - add more of the structure - create_checked_childless_subintent_v2(&network); + let (signed_transaction_intent, signed_transaction_intent_hash) = + create_signed_transaction_intent_v2(&network); + let (notary_signature, notary_signature_hash) = + create_notary_signature_v2(signed_transaction_intent_hash); + + let notarized_transaction = NotarizedTransactionV2 { + signed_transaction_intent, + notary_signature, + }; + let expected_hash = NotarizedTransactionHash(hash( + [ + [ + TRANSACTION_HASHABLE_PAYLOAD_PREFIX, + TransactionDiscriminator::V2Notarized as u8, + ] + .as_slice(), + signed_transaction_intent_hash.0.as_slice(), + notary_signature_hash.0.as_slice(), + ] + .concat(), + )); + let raw = notarized_transaction.to_raw().unwrap(); + + let prepared_transaction = notarized_transaction + .prepare(&PreparationSettings::latest()) + .unwrap(); + let actual_transaction_intent_hash = prepared_transaction.transaction_intent_hash(); + let actual_signed_transaction_intent_hash = + prepared_transaction.signed_transaction_intent_hash(); + let notarized_transaction_hash = prepared_transaction.notarized_transaction_hash(); + + assert_eq!(expected_hash, notarized_transaction_hash); + assert_eq!( + notarized_transaction_hash.to_string(&TransactionHashBech32Encoder::for_simulator()), + "notarizedtransaction_sim1qh37lkr547jgv5zfvlkq4njdhn62m2sg09k6njmkuma7u2hd4zasrmhyew", + ); + assert_eq!( + actual_signed_transaction_intent_hash + .to_string(&TransactionHashBech32Encoder::for_simulator()), + "signedintent_sim1z2at9wmfh7pcx7ad0c4npyv3xn3mecf2gyehwd6g6w99v56ntfsq4k92yx", + ); + assert_eq!( + actual_transaction_intent_hash + .to_string(&TransactionHashBech32Encoder::for_simulator()), + "txid_sim1v7xlgxkrk59qekpj53x8jul0lml0r4nzn3yfmv4jd5ysjewmkaust5l3t2", + ); + assert_eq!( + hex::encode(raw.as_slice()), + "4d220c0221032103210322010120072009b3f25a3a1839f46ddb09b068271811f6f00a79246fb24e7a808a9e46d6075d010009000000002105210607f20a01000000000000000a0a000000000000002200002201010500000000000000000a00000000000000002020020704000102030702050622010121020c0a746578742f706c61696e2200010c0c48656c6c6f20776f726c64212020010720b37d9be9fe7362e9f01a828af77a3298758ac7d43be750575befdbd395c28918202201610209000000002100202101012105210607f20a01000000000000000a0a000000000000002200002201010500000000000000000a00000000000000002020020704000102030702050622010121020c0a746578742f706c61696e2200010c0c48656c6c6f20776f726c64212020002022016001210020220101022007204d956b5eb1147b3a80c40170e340e2918d2a9f33bdb529c54401e3ed80a4e70a2101200740e04f0e563d71ca150d900d75538d2253dff0f77d86c8ecfa4dcd25ac94de5a4ed27d76ac95c3ee8ebdcc1da52df6d1ca5f265bc1f973f631bc753e4146b3aa0c20200122010102200720c561fa9f643fe5c60113cce9db282fde2b9e5ca5fc6b6fc0d1679bb339c9f72f2101200740860417490e96c91addd5a390f5f1bcd2697535f23a947d2337b291a7b86611f56cc3aa0606ac8b8cba98381c35ef9a1f655362b18764eb90b1d8b814ec17f40e2201012101200740975a47326156a7818b4776e3e455a67c906c34eda7a9c9bb9688c77664ed9679c78aa9e33740aa1d3631b89119071a3feaf02b650799da64da7f659d107db905" + ); + + // Check that the transaction we created is actually valid... + prepared_transaction + .validate(&TransactionValidator::new_for_latest_simulator()) + .unwrap(); } - fn create_checked_childless_subintent_v2( + fn create_notary_signature_v2( + hash_to_sign: SignedTransactionIntentHash, + ) -> (NotarySignatureV2, Hash) { + let notary_signature = NotarySignatureV2( + TransactionV2Builder::testing_default_notary() + .sign_without_public_key(hash_to_sign.as_hash()), + ); + let expected_hash = hash_encoded_sbor_value_body(¬ary_signature); + let actual_hash = hash_from_partial_prepare(¬ary_signature); + assert_eq!(expected_hash, actual_hash); + (notary_signature, actual_hash) + } + + fn create_signed_transaction_intent_v2( network: &NetworkDefinition, - ) -> (SubintentV2, SubintentHash) { - let (header, expected_header_hash) = create_intent_header_v2(network); - let (blobs, expected_blobs_hash) = create_blobs_v1(); - let (instructions, expected_instructions_hash) = - create_childless_subintent_instructions_v2(); - let (message, expected_message_hash) = create_message_v2(); - let (child_intent_constraints, expected_constraints_hash) = - create_childless_child_intents_v2(); - - let subintent = SubintentV2 { - intent_core: IntentCoreV2 { - header, - instructions, - blobs, - message, - children: child_intent_constraints, - }, + ) -> (SignedTransactionIntentV2, SignedTransactionIntentHash) { + let (transaction_intent, transaction_intent_hash, subintent_hash) = + create_transaction_intent_v2(&network); + let (transaction_intent_signatures, transaction_intent_signatures_hash) = + create_intent_signatures_v2(vec![2313], transaction_intent_hash); + let (non_root_subintent_signatures, non_root_subintent_signatures_hash) = + create_non_root_subintent_signatures(vec![subintent_hash]); + + let signed = SignedTransactionIntentV2 { + transaction_intent, + transaction_intent_signatures, + non_root_subintent_signatures, }; - let expected_intent_core_hash = hash( + + let expected_hash = SignedTransactionIntentHash(hash( [ - expected_header_hash.as_slice(), - expected_blobs_hash.as_slice(), - expected_message_hash.as_slice(), - expected_constraints_hash.as_slice(), - expected_instructions_hash.as_slice(), + [ + TRANSACTION_HASHABLE_PAYLOAD_PREFIX, + TransactionDiscriminator::V2SignedTransactionIntent as u8, + ] + .as_slice(), + transaction_intent_hash.0.as_slice(), + transaction_intent_signatures_hash.0.as_slice(), + non_root_subintent_signatures_hash.0.as_slice(), ] .concat(), + )); + + let prepared = signed.prepare(&PreparationSettings::latest()).unwrap(); + let actual_hash = prepared.signed_transaction_intent_hash(); + assert_eq!( + actual_hash.to_string(&TransactionHashBech32Encoder::for_simulator()), + "signedintent_sim1z2at9wmfh7pcx7ad0c4npyv3xn3mecf2gyehwd6g6w99v56ntfsq4k92yx", + ); + assert_eq!(expected_hash, actual_hash); + + (signed, actual_hash) + } + + fn create_non_root_subintent_signatures( + subintent_hashes: Vec, + ) -> (NonRootSubintentSignaturesV2, Hash) { + let (batches, batch_hashes): (Vec<_>, Vec<_>) = subintent_hashes + .into_iter() + .enumerate() + .map(|(i, subintent_hash)| { + create_intent_signatures_v2(vec![(i * 100 + 42) as u64], subintent_hash) + }) + .unzip(); + let signature_batches = NonRootSubintentSignaturesV2 { + by_subintent: batches, + }; + let expected_hash = hash_contatenated_hashes(batch_hashes); + let actual_hash = hash_from_partial_prepare(&signature_batches); + assert_eq!(expected_hash, actual_hash); + (signature_batches, expected_hash) + } + + fn create_intent_signatures_v2( + key_sources: Vec, + intent_hash: impl Into, + ) -> (IntentSignaturesV2, Hash) { + let hash_to_sign = intent_hash.into().into_hash(); + let signatures = IntentSignaturesV2 { + signatures: key_sources + .into_iter() + .map(|key_source| { + create_intent_signature_v1( + Ed25519PrivateKey::from_u64(key_source).unwrap(), + &hash_to_sign, + ) + }) + .collect(), + }; + let expected_hash = hash_encoded_sbor_value_body(&signatures); + let actual_hash = hash_from_partial_prepare(&signatures); + assert_eq!(expected_hash, actual_hash); + (signatures, actual_hash) + } + + fn create_intent_signature_v1(signer: impl Signer, hash_to_sign: &Hash) -> IntentSignatureV1 { + let signature = signer.sign_with_public_key(hash_to_sign); + IntentSignatureV1(signature) + } + + fn create_transaction_intent_v2( + network: &NetworkDefinition, + ) -> (TransactionIntentV2, TransactionIntentHash, SubintentHash) { + let (subintent_1, subintent_1_hash) = create_checked_childless_subintent_v2(&network); + let (non_root_subintents, non_root_subintents_hash) = + create_non_root_subintents_v2(vec![subintent_1], vec![subintent_1_hash]); + + let (transaction_header, transaction_header_hash) = create_transaction_header_v2(); + let (root_intent_core, root_intent_core_hash) = create_intent_core_v2( + &NetworkDefinition::simulator(), + vec![InstructionV2::YieldToChild(YieldToChild::empty(0))], + vec![subintent_1_hash], ); + + let expected_transaction_intent_hash = TransactionIntentHash(hash( + [ + [ + TRANSACTION_HASHABLE_PAYLOAD_PREFIX, + TransactionDiscriminator::V2TransactionIntent as u8, + ] + .as_slice(), + transaction_header_hash.as_slice(), + root_intent_core_hash.as_slice(), + non_root_subintents_hash.as_slice(), + ] + .concat(), + )); + + let transaction_intent = TransactionIntentV2 { + transaction_header, + root_intent_core, + non_root_subintents, + }; + + let actual_hash = transaction_intent + .prepare(PreparationSettings::latest_ref()) + .unwrap() + .transaction_intent_hash(); + + assert_eq!(expected_transaction_intent_hash, actual_hash); + assert_eq!( + expected_transaction_intent_hash + .to_string(&TransactionHashBech32Encoder::for_simulator()), + "txid_sim1v7xlgxkrk59qekpj53x8jul0lml0r4nzn3yfmv4jd5ysjewmkaust5l3t2", + ); + + (transaction_intent, actual_hash, subintent_1_hash) + } + + fn create_transaction_header_v2() -> (TransactionHeaderV2, Hash) { + let transaction_header = TransactionHeaderV2 { + notary_public_key: TransactionV2Builder::testing_default_notary() + .public_key() + .into(), + notary_is_signatory: false, + tip_basis_points: 0, + }; + let expected_hash = hash_encoded_sbor_value_body(&transaction_header); + let actual_hash = hash_from_partial_prepare(&transaction_header); + assert_eq!(expected_hash, actual_hash); + (transaction_header, expected_hash) + } + + fn create_non_root_subintents_v2( + subintents: Vec, + hashes: Vec, + ) -> (NonRootSubintentsV2, Hash) { + let non_root_subintents = NonRootSubintentsV2(subintents); + + let expected_hash = hash_contatenated_hashes(hashes); + let actual_hash = hash_from_partial_prepare(&non_root_subintents); + assert_eq!(expected_hash, actual_hash); + + (non_root_subintents, expected_hash) + } + + fn create_checked_childless_subintent_v2( + network: &NetworkDefinition, + ) -> (SubintentV2, SubintentHash) { + let (intent_core, intent_core_hash) = create_intent_core_v2( + network, + vec![InstructionV2::YieldToParent(YieldToParent::empty())], + vec![], + ); + + let subintent = SubintentV2 { intent_core }; + let expected_subintent_hash = SubintentHash(hash( [ [ @@ -388,7 +617,7 @@ mod tests { TransactionDiscriminator::V2Subintent as u8, ] .as_slice(), - expected_intent_core_hash.as_slice(), + intent_core_hash.as_slice(), ] .concat(), )); @@ -400,12 +629,48 @@ mod tests { assert_eq!(expected_subintent_hash, actual_subintent_hash); assert_eq!( expected_subintent_hash.to_string(&TransactionHashBech32Encoder::for_simulator()), - "subtxid_sim1ree59h2u2sguzl6g72pn7q9hpe3r28l95c05f2rfe7cgfp4sgmwqx5l3mu", + "subtxid_sim1kd7eh607wd3wnuq6s290w73jnp6c437580n4q46malda89wz3yvq3cph38", ); (subintent, actual_subintent_hash) } + fn create_intent_core_v2( + network: &NetworkDefinition, + instructions: Vec, + children: Vec, + ) -> (IntentCoreV2, Hash) { + let (header, expected_header_hash) = create_intent_header_v2(network); + let (blobs, expected_blobs_hash) = create_blobs_v1(); + let (instructions, expected_instructions_hash) = + create_subintent_instructions_v2(instructions); + let (message, expected_message_hash) = create_message_v2(); + let (child_intent_constraints, expected_constraints_hash) = + create_child_subintent_specifiers_v2(children); + + let intent_core = IntentCoreV2 { + header, + instructions, + blobs, + message, + children: child_intent_constraints, + }; + + let expected_hash = hash( + [ + expected_header_hash.as_slice(), + expected_blobs_hash.as_slice(), + expected_message_hash.as_slice(), + expected_constraints_hash.as_slice(), + expected_instructions_hash.as_slice(), + ] + .concat(), + ); + let actual_hash = hash_from_partial_prepare(&intent_core); + assert_eq!(expected_hash, actual_hash); + (intent_core, expected_hash) + } + fn create_intent_header_v2(network: &NetworkDefinition) -> (IntentHeaderV2, Hash) { let intent_header = IntentHeaderV2 { network_id: network.id, @@ -416,11 +681,7 @@ mod tests { intent_discriminator: 0, }; let expected_hash = hash_encoded_sbor_value_body(&intent_header); - let actual_hash = intent_header - .prepare_partial(PreparationSettings::latest_ref()) - .unwrap() - .get_summary() - .hash; + let actual_hash = hash_from_partial_prepare(&intent_header); assert_eq!(expected_hash, actual_hash); (intent_header, expected_hash) } @@ -428,31 +689,24 @@ mod tests { fn create_blobs_v1() -> (BlobsV1, Hash) { let blob1: Vec = vec![0, 1, 2, 3]; let blob2: Vec = vec![5, 6]; - let expected_hash = hash([hash(&blob1).0.as_slice(), hash(&blob2).0.as_slice()].concat()); + let expected_hash = hash_contatenated_hashes([hash(&blob1), hash(&blob2)]); let blobs_v1 = BlobsV1 { blobs: vec![BlobV1(blob1), BlobV1(blob2)], }; - let actual_hash = blobs_v1 - .prepare_partial(PreparationSettings::latest_ref()) - .unwrap() - .get_summary() - .hash; + let actual_hash = hash_from_partial_prepare(&blobs_v1); assert_eq!(expected_hash, actual_hash); (blobs_v1, expected_hash) } - fn create_childless_subintent_instructions_v2() -> (InstructionsV2, Hash) { - let instructions = InstructionsV2::from(vec![]); + fn create_subintent_instructions_v2( + instructions: Vec, + ) -> (InstructionsV2, Hash) { + let instructions = InstructionsV2::from(instructions); let expected_hash = hash_encoded_sbor_value_body(&instructions); - - let actual_hash = instructions - .prepare_partial(PreparationSettings::latest_ref()) - .unwrap() - .get_summary() - .hash; + let actual_hash = hash_from_partial_prepare(&instructions); assert_eq!(expected_hash, actual_hash); (instructions, expected_hash) @@ -462,37 +716,29 @@ mod tests { let message = MessageV2::Plaintext(PlaintextMessageV1::text("Hello world!")); let expected_hash = hash_encoded_sbor_value_body(&message); - let actual_hash = message - .prepare_partial(PreparationSettings::latest_ref()) - .unwrap() - .get_summary() - .hash; + let actual_hash = hash_from_partial_prepare(&message); assert_eq!(expected_hash, actual_hash); (message, expected_hash) } - fn create_childless_child_intents_v2() -> (ChildSubintentSpecifiersV2, Hash) { - let children: ChildSubintentSpecifiersV2 = ChildSubintentSpecifiersV2 { - children: Default::default(), + fn create_child_subintent_specifiers_v2( + children: Vec, + ) -> (ChildSubintentSpecifiersV2, Hash) { + let child_subintent_specifiers: ChildSubintentSpecifiersV2 = ChildSubintentSpecifiersV2 { + children: children.clone().into_iter().map(|h| h.into()).collect(), }; - // Concatenation of all hashes - let empty: [u8; 0] = []; - let expected_hash = hash(&empty); - - let actual_hash = children - .prepare_partial(PreparationSettings::latest_ref()) - .unwrap() - .get_summary() - .hash; + let expected_hash = hash_contatenated_hashes(children); + let actual_hash = hash_from_partial_prepare(&child_subintent_specifiers); assert_eq!(expected_hash, actual_hash); - (children, expected_hash) + (child_subintent_specifiers, expected_hash) } /// This test demonstrates how the hashes and payloads are constructed in a valid system transaction. /// A system transaction can be embedded into the node's LedgerTransaction structure, eg as part of Genesis #[test] + #[allow(deprecated)] // Transaction V1 is allowed to use deprecated hashing pub fn v1_system_transaction_structure() { let instructions = vec![InstructionV1::DropAuthZoneProofs(DropAuthZoneProofs)]; let expected_instructions_hash = hash_encoded_sbor_value(&instructions); diff --git a/radix-transactions/src/model/v2/intent_signatures_v2.rs b/radix-transactions/src/model/v2/intent_signatures_v2.rs index bebcafd2cc..da31c54c36 100644 --- a/radix-transactions/src/model/v2/intent_signatures_v2.rs +++ b/radix-transactions/src/model/v2/intent_signatures_v2.rs @@ -31,6 +31,10 @@ pub struct NonRootSubintentSignaturesV2 { pub by_subintent: Vec, } +impl TransactionPartialPrepare for NonRootSubintentSignaturesV2 { + type Prepared = PreparedNonRootSubintentSignaturesV2; +} + #[derive(Debug, Clone, Eq, PartialEq)] pub struct PreparedNonRootSubintentSignaturesV2 { pub by_subintent: Vec, diff --git a/radix-transactions/src/model/v2/notarized_transaction_v2.rs b/radix-transactions/src/model/v2/notarized_transaction_v2.rs index 3bb6facbcd..e3af87b465 100644 --- a/radix-transactions/src/model/v2/notarized_transaction_v2.rs +++ b/radix-transactions/src/model/v2/notarized_transaction_v2.rs @@ -97,6 +97,10 @@ define_transaction_payload!( #[sbor(transparent)] pub struct NotarySignatureV2(pub SignatureV1); +impl TransactionPartialPrepare for NotarySignatureV2 { + type Prepared = PreparedNotarySignatureV2; +} + #[allow(deprecated)] pub type PreparedNotarySignatureV2 = SummarizedRawValueBody; From 0e3a86900198c07e1607382e27838b0215f9b5d1 Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 26 Nov 2024 13:35:05 +0000 Subject: [PATCH 2/4] tests: Added tests for encoding/decoding manifests --- .../src/manifest/any_manifest.rs | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/radix-transactions/src/manifest/any_manifest.rs b/radix-transactions/src/manifest/any_manifest.rs index 6dc2e06c36..7d2a686045 100644 --- a/radix-transactions/src/manifest/any_manifest.rs +++ b/radix-transactions/src/manifest/any_manifest.rs @@ -90,6 +90,51 @@ impl TryFrom for SubintentManifestV2 { } } +pub trait ManifestPayload: + Into + + TryFrom + + for<'a> ManifestSborEnumVariantFor< + AnyManifest, + OwnedVariant: ManifestDecode, + BorrowedVariant<'a>: ManifestEncode, + > +{ + fn to_raw(self) -> Result { + Ok(manifest_encode(&self.as_encodable_variant())?.into()) + } + + fn to_canonical_bytes(self) -> Result, EncodeError> { + self.to_raw().map(|raw| raw.0) + } + + fn from_raw(raw: &RawManifest) -> Result { + Ok(Self::from_decoded_variant(manifest_decode(raw.as_ref())?)) + } + + fn from_bytes(bytes: &[u8]) -> Result { + AnyManifest::attempt_decode_from_arbitrary_payload(bytes)? + .try_into() + .map_err(|_| { + format!( + "Manifest wasn't of the expected type: {}.", + std::any::type_name::() + ) + }) + } +} + +impl< + M: Into + + TryFrom + + for<'a> ManifestSborEnumVariantFor< + AnyManifest, + OwnedVariant: ManifestDecode, + BorrowedVariant<'a>: ManifestEncode, + >, + > ManifestPayload for M +{ +} + // It's not technically a conventional transaction payload, but let's reuse the macro define_raw_transaction_payload!(RawManifest, TransactionPayloadKind::Other); @@ -98,10 +143,18 @@ impl AnyManifest { Ok(RawManifest::from_vec(manifest_encode(self)?)) } + pub fn to_canonical_bytes(self) -> Result, EncodeError> { + self.to_raw().map(|raw| raw.0) + } + pub fn from_raw(raw: &RawManifest) -> Result { Ok(manifest_decode(raw.as_slice())?) } + pub fn from_bytes(bytes: &[u8]) -> Result { + AnyManifest::attempt_decode_from_arbitrary_payload(bytes) + } + pub fn attempt_decode_from_arbitrary_payload(bytes: &[u8]) -> Result { // First, try to decode as AnyManifest if let Ok(any_manifest) = manifest_decode::(bytes) { @@ -296,3 +349,51 @@ impl TryFrom<&str> for ManifestKind { Ok(kind) } } + +#[cfg(test)] +mod tests { + use crate::internal_prelude::*; + + #[test] + pub fn subintent_manifest_v2_is_round_trip_encodable_and_fixed() { + let builder = ManifestBuilder::new_subintent_v2(); + let lookup = builder.name_lookup(); + + // We include an object name to check that gets preserved + let manifest = builder + .take_all_from_worktop(XRD, "my_bucket") + .yield_to_parent((lookup.bucket("my_bucket"),)) + .build(); + let encoded = manifest.clone().to_raw().unwrap(); + let decoded = SubintentManifestV2::from_raw(&encoded).unwrap(); + assert_eq!(manifest, decoded); + + // Ensuring that old encoded manifests can be decoded is required to ensure that manifests + // saved with `rtmc` can still be read with `rtmd` + let cuttlefish_hex = "4d2203012104202202020180005da66318c6318c61f5a61b4c6318c6318cf794aa8d295f14e6318c6318c660012101810000000023202000202000220101230c230509616464726573736573090c00076275636b657473090c0100000000096d795f6275636b657407696e74656e7473090c000670726f6f6673090c000c7265736572766174696f6e73090c00"; + let cuttlefish_raw = RawManifest::from_hex(cuttlefish_hex).unwrap(); + let cuttlefish_decoded = SubintentManifestV2::from_raw(&cuttlefish_raw).unwrap(); + assert_eq!(manifest, cuttlefish_decoded); + } + + #[test] + pub fn transaction_intent_manifest_v2_is_round_trip_encodable_and_fixed() { + let builder = ManifestBuilder::new_v2(); + + // We include an object name to check that gets preserved + let manifest = builder + .lock_fee_from_faucet() + .create_proof_from_auth_zone_of_all(XRD, "my_proof") + .build(); + let encoded = manifest.clone().to_raw().unwrap(); + let decoded = TransactionManifestV2::from_raw(&encoded).unwrap(); + assert_eq!(manifest, decoded); + + // Ensuring that old encoded manifests can be decoded is required to ensure that manifests + // saved with `rtmc` can still be read with `rtmd` + let cuttlefish_hex = "4d220201210420220241038000c0566318c6318c64f798cacc6318c6318cf7be8af78a78f8a6318c6318c60c086c6f636b5f66656521018500002059dd64f00c0f010000000000000000000000000000160180005da66318c6318c61f5a61b4c6318c6318cf794aa8d295f14e6318c6318c623202000202000220101230c230509616464726573736573090c00076275636b657473090c0007696e74656e7473090c000670726f6f6673090c0100000000086d795f70726f6f660c7265736572766174696f6e73090c00"; + let cuttlefish_raw = RawManifest::from_hex(cuttlefish_hex).unwrap(); + let cuttlefish_decoded = TransactionManifestV2::from_raw(&cuttlefish_raw).unwrap(); + assert_eq!(manifest, cuttlefish_decoded); + } +} From 1b5c173b3295ff96fe3635bc3495a1df7b5565a7 Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 26 Nov 2024 13:46:14 +0000 Subject: [PATCH 3/4] tweak: `ContextualFormat` takes a `fmt::Formatter` --- .../src/types/addresses/component_address.rs | 4 ++-- .../src/types/addresses/global_address.rs | 4 ++-- .../src/types/addresses/internal_address.rs | 4 ++-- .../src/types/addresses/package_address.rs | 4 ++-- .../src/types/addresses/resource_address.rs | 4 ++-- radix-common/src/types/blueprint_id.rs | 4 ++-- radix-common/src/types/node_and_substate.rs | 4 ++-- radix-common/src/types/non_fungible_global_id.rs | 4 ++-- radix-engine-interface/src/types/event_id.rs | 4 ++-- .../src/types/indexed_value.rs | 4 ++-- radix-engine-interface/src/types/invocation.rs | 4 ++-- radix-engine/src/errors.rs | 16 ++++++++-------- .../src/transaction/transaction_receipt.rs | 8 ++++---- radix-rust/src/contextual_display.rs | 8 ++++---- radix-transaction-scenarios/src/scenario.rs | 4 ++-- radix-transactions/src/data/formatter.rs | 8 ++++---- radix-transactions/src/model/hash/display.rs | 4 ++-- .../display/contextual_display.rs | 8 ++++---- 18 files changed, 50 insertions(+), 50 deletions(-) diff --git a/radix-common/src/types/addresses/component_address.rs b/radix-common/src/types/addresses/component_address.rs index 7cb89ff717..5e9aff917d 100644 --- a/radix-common/src/types/addresses/component_address.rs +++ b/radix-common/src/types/addresses/component_address.rs @@ -291,9 +291,9 @@ impl fmt::Debug for ComponentAddress { impl<'a> ContextualDisplay> for ComponentAddress { type Error = AddressBech32EncodeError; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &AddressDisplayContext<'a>, ) -> Result<(), Self::Error> { if let Some(encoder) = context.encoder { diff --git a/radix-common/src/types/addresses/global_address.rs b/radix-common/src/types/addresses/global_address.rs index 1cf38d2e3e..17867eebe8 100644 --- a/radix-common/src/types/addresses/global_address.rs +++ b/radix-common/src/types/addresses/global_address.rs @@ -241,9 +241,9 @@ impl fmt::Debug for GlobalAddress { impl<'a> ContextualDisplay> for GlobalAddress { type Error = AddressBech32EncodeError; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &AddressDisplayContext<'a>, ) -> Result<(), Self::Error> { if let Some(encoder) = context.encoder { diff --git a/radix-common/src/types/addresses/internal_address.rs b/radix-common/src/types/addresses/internal_address.rs index 1a6f92d897..4aa3aec4c0 100644 --- a/radix-common/src/types/addresses/internal_address.rs +++ b/radix-common/src/types/addresses/internal_address.rs @@ -231,9 +231,9 @@ impl fmt::Debug for InternalAddress { impl<'a> ContextualDisplay> for InternalAddress { type Error = AddressBech32EncodeError; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &AddressDisplayContext<'a>, ) -> Result<(), Self::Error> { if let Some(encoder) = context.encoder { diff --git a/radix-common/src/types/addresses/package_address.rs b/radix-common/src/types/addresses/package_address.rs index dffb24138d..fb4d30c7c0 100644 --- a/radix-common/src/types/addresses/package_address.rs +++ b/radix-common/src/types/addresses/package_address.rs @@ -255,9 +255,9 @@ impl fmt::Debug for PackageAddress { impl<'a> ContextualDisplay> for PackageAddress { type Error = AddressBech32EncodeError; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &AddressDisplayContext<'a>, ) -> Result<(), Self::Error> { if let Some(encoder) = context.encoder { diff --git a/radix-common/src/types/addresses/resource_address.rs b/radix-common/src/types/addresses/resource_address.rs index e1a888e5fd..535a6046a5 100644 --- a/radix-common/src/types/addresses/resource_address.rs +++ b/radix-common/src/types/addresses/resource_address.rs @@ -248,9 +248,9 @@ impl fmt::Debug for ResourceAddress { impl<'a> ContextualDisplay> for ResourceAddress { type Error = AddressBech32EncodeError; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &AddressDisplayContext<'a>, ) -> Result<(), Self::Error> { if let Some(encoder) = context.encoder { diff --git a/radix-common/src/types/blueprint_id.rs b/radix-common/src/types/blueprint_id.rs index 2af9eeb29a..3b377239d8 100644 --- a/radix-common/src/types/blueprint_id.rs +++ b/radix-common/src/types/blueprint_id.rs @@ -27,9 +27,9 @@ impl BlueprintId { impl<'a> ContextualDisplay> for BlueprintId { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &AddressDisplayContext<'a>, ) -> Result<(), Self::Error> { write!( diff --git a/radix-common/src/types/node_and_substate.rs b/radix-common/src/types/node_and_substate.rs index 1c8eebf5ce..2dd3a02f03 100644 --- a/radix-common/src/types/node_and_substate.rs +++ b/radix-common/src/types/node_and_substate.rs @@ -203,9 +203,9 @@ impl Debug for NodeId { impl<'a> ContextualDisplay> for NodeId { type Error = AddressBech32EncodeError; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &AddressDisplayContext<'a>, ) -> Result<(), Self::Error> { if let Some(encoder) = context.encoder { diff --git a/radix-common/src/types/non_fungible_global_id.rs b/radix-common/src/types/non_fungible_global_id.rs index f0a3f89cf2..3b50d73ab2 100644 --- a/radix-common/src/types/non_fungible_global_id.rs +++ b/radix-common/src/types/non_fungible_global_id.rs @@ -155,9 +155,9 @@ impl fmt::Display for ParseNonFungibleGlobalIdError { impl<'a> ContextualDisplay> for NonFungibleGlobalId { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &AddressDisplayContext<'a>, ) -> Result<(), Self::Error> { write!( diff --git a/radix-engine-interface/src/types/event_id.rs b/radix-engine-interface/src/types/event_id.rs index ea3cb52a69..4c299ee15e 100644 --- a/radix-engine-interface/src/types/event_id.rs +++ b/radix-engine-interface/src/types/event_id.rs @@ -27,9 +27,9 @@ pub enum Emitter { impl<'a> ContextualDisplay> for Emitter { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &AddressDisplayContext<'a>, ) -> Result<(), Self::Error> { match self { diff --git a/radix-engine-interface/src/types/indexed_value.rs b/radix-engine-interface/src/types/indexed_value.rs index 5482969c3a..6a23469c33 100644 --- a/radix-engine-interface/src/types/indexed_value.rs +++ b/radix-engine-interface/src/types/indexed_value.rs @@ -165,9 +165,9 @@ impl<'s, 'a> ContextualDisplay( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &ValueDisplayParameters<'_, '_, ScryptoCustomExtension>, ) -> Result<(), Self::Error> { ScryptoRawPayload::new_from_valid_slice(self.as_slice()).format(f, *context) diff --git a/radix-engine-interface/src/types/invocation.rs b/radix-engine-interface/src/types/invocation.rs index 1369fadf00..72e872937e 100644 --- a/radix-engine-interface/src/types/invocation.rs +++ b/radix-engine-interface/src/types/invocation.rs @@ -15,9 +15,9 @@ pub struct FnIdentifier { impl<'a> ContextualDisplay> for FnIdentifier { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &AddressDisplayContext<'a>, ) -> Result<(), Self::Error> { write!( diff --git a/radix-engine/src/errors.rs b/radix-engine/src/errors.rs index 4ea822ee07..9922036298 100644 --- a/radix-engine/src/errors.rs +++ b/radix-engine/src/errors.rs @@ -144,9 +144,9 @@ pub enum RejectionReason { impl<'a> ContextualDisplay> for RejectionReason { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &ScryptoValueDisplayContext, ) -> Result<(), Self::Error> { self.create_persistable().contextual_format(f, context) @@ -172,9 +172,9 @@ impl<'a> ContextualDisplay> for PersistableReject type Error = fmt::Error; /// See [`SerializableRuntimeError::contextual_format`] for more information. - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &ScryptoValueDisplayContext, ) -> Result<(), Self::Error> { let value = &self.encoded_rejection_reason; @@ -339,9 +339,9 @@ pub enum RuntimeError { impl<'a> ContextualDisplay> for RuntimeError { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &ScryptoValueDisplayContext, ) -> Result<(), Self::Error> { self.create_persistable().contextual_format(f, context) @@ -383,9 +383,9 @@ pub struct PersistableRuntimeError { impl<'a> ContextualDisplay> for PersistableRuntimeError { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &ScryptoValueDisplayContext, ) -> Result<(), Self::Error> { let value = &self.encoded_error; diff --git a/radix-engine/src/transaction/transaction_receipt.rs b/radix-engine/src/transaction/transaction_receipt.rs index 29d508420f..7468e0560e 100644 --- a/radix-engine/src/transaction/transaction_receipt.rs +++ b/radix-engine/src/transaction/transaction_receipt.rs @@ -1020,9 +1020,9 @@ impl<'a> TransactionReceiptDisplayContextBuilder<'a> { impl<'a> ContextualDisplay> for TransactionReceipt { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &TransactionReceiptDisplayContext<'a>, ) -> Result<(), Self::Error> { let result = &self.result; @@ -1195,9 +1195,9 @@ impl<'a, 'b> ContextualDisplay> { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &TransactionReceiptDisplayContext<'a>, ) -> Result<(), Self::Error> { let state_updates = self.0; diff --git a/radix-rust/src/contextual_display.rs b/radix-rust/src/contextual_display.rs index 47785e5b55..42cd18797e 100644 --- a/radix-rust/src/contextual_display.rs +++ b/radix-rust/src/contextual_display.rs @@ -20,9 +20,9 @@ pub trait ContextualDisplay { /// instead of a `&Context`. /// /// [`format`]: #method.format - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &Context, ) -> Result<(), Self::Error>; @@ -34,9 +34,9 @@ pub trait ContextualDisplay { /// /// [`contextual_format`]: #method.contextual_format /// [`display`]: #method.display - fn format>( + fn format>( &self, - f: &mut F, + f: &mut fmt::Formatter, context: TContext, ) -> Result<(), Self::Error> { self.contextual_format(f, &context.into()) diff --git a/radix-transaction-scenarios/src/scenario.rs b/radix-transaction-scenarios/src/scenario.rs index 02f77aa006..3d741f8453 100644 --- a/radix-transaction-scenarios/src/scenario.rs +++ b/radix-transaction-scenarios/src/scenario.rs @@ -417,9 +417,9 @@ pub enum DescribedAddress { impl<'a> ContextualDisplay> for DescribedAddress { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &AddressDisplayContext<'a>, ) -> Result<(), Self::Error> { match self { diff --git a/radix-transactions/src/data/formatter.rs b/radix-transactions/src/data/formatter.rs index 946aaac6bb..bc1ab0d8bc 100644 --- a/radix-transactions/src/data/formatter.rs +++ b/radix-transactions/src/data/formatter.rs @@ -83,9 +83,9 @@ impl<'a> Into> for Option<&'a AddressBec impl<'a> ContextualDisplay> for ManifestValue { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &ManifestDecompilationDisplayContext<'a>, ) -> Result<(), Self::Error> { format_manifest_value(f, self, context, false, 0) @@ -515,9 +515,9 @@ impl<'a> fmt::Display for DisplayableManifestValueKind<'a> { impl<'a> ContextualDisplay> for ManifestCustomValue { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &ManifestDecompilationDisplayContext<'a>, ) -> Result<(), Self::Error> { format_custom_value(f, self, context, false, 0) diff --git a/radix-transactions/src/model/hash/display.rs b/radix-transactions/src/model/hash/display.rs index 7f554e40f5..48b910d95f 100644 --- a/radix-transactions/src/model/hash/display.rs +++ b/radix-transactions/src/model/hash/display.rs @@ -31,9 +31,9 @@ macro_rules! impl_contextual_display { impl<'a> ContextualDisplay> for $type { type Error = fmt::Error; - fn contextual_format( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, context: &TransactionHashDisplayContext<'a>, ) -> Result<(), Self::Error> { if let Some(encoder) = context.encoder { diff --git a/sbor/src/representations/display/contextual_display.rs b/sbor/src/representations/display/contextual_display.rs index 4f1dc13934..679c726dd5 100644 --- a/sbor/src/representations/display/contextual_display.rs +++ b/sbor/src/representations/display/contextual_display.rs @@ -141,9 +141,9 @@ impl<'s, 'a, 'b, E: FormattableCustomExtension> ContextualDisplay( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, options: &ValueDisplayParameters<'s, 'a, E>, ) -> Result<(), Self::Error> { let context = options.get_context_and_type_id(); @@ -171,9 +171,9 @@ impl<'s, 'a, 'b, E: FormattableCustomExtension> ContextualDisplay( + fn contextual_format( &self, - f: &mut F, + f: &mut fmt::Formatter, options: &ValueDisplayParameters<'s, 'a, E>, ) -> Result<(), Self::Error> { let context = options.get_context_and_type_id(); From 9c98fdedce41d075320770734b217538a9b5898e Mon Sep 17 00:00:00 2001 From: David Edey Date: Tue, 26 Nov 2024 14:04:56 +0000 Subject: [PATCH 4/4] feat: Add `ContextualDisplay` to `TransactionValidationError` --- radix-rust/src/contextual_display.rs | 34 ++++++++++ radix-transactions/src/errors.rs | 93 ++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/radix-rust/src/contextual_display.rs b/radix-rust/src/contextual_display.rs index 42cd18797e..17ca431f6a 100644 --- a/radix-rust/src/contextual_display.rs +++ b/radix-rust/src/contextual_display.rs @@ -70,6 +70,21 @@ pub trait ContextualDisplay { } } + /// Returns an object implementing [`fmt::Debug`] using the contextual display implementation. + /// + /// Typically you should use [`format`] instead. + /// + /// [`format`]: #method.format + fn debug_as_display<'a, 'b, TContext: Into>( + &'a self, + context: TContext, + ) -> ContextDebuggableAsDisplay<'a, Self, Context> { + ContextDebuggableAsDisplay { + value: self, + context: context.into(), + } + } + fn to_string<'a, 'b, TContext: Into>(&'a self, context: TContext) -> String { self.display(context).to_string() } @@ -93,3 +108,22 @@ where .map_err(|_| fmt::Error) // We eat any errors into fmt::Error } } + +pub struct ContextDebuggableAsDisplay<'a, TValue, TContext> +where + TValue: ContextualDisplay + ?Sized, +{ + value: &'a TValue, + context: TContext, +} + +impl<'a, 'b, TValue, TContext> fmt::Debug for ContextDebuggableAsDisplay<'a, TValue, TContext> +where + TValue: ContextualDisplay + ?Sized, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + self.value + .contextual_format(f, &self.context) + .map_err(|_| fmt::Error) // We eat any errors into fmt::Error + } +} diff --git a/radix-transactions/src/errors.rs b/radix-transactions/src/errors.rs index 681155d2d5..7152cc6706 100644 --- a/radix-transactions/src/errors.rs +++ b/radix-transactions/src/errors.rs @@ -69,6 +69,41 @@ pub enum TransactionValidationError { SignatureValidationError(TransactionValidationErrorLocation, SignatureValidationError), } +impl<'a> ContextualDisplay> for TransactionValidationError { + type Error = fmt::Error; + + fn contextual_format( + &self, + f: &mut fmt::Formatter, + context: &TransactionHashDisplayContext<'a>, + ) -> Result<(), Self::Error> { + match self { + Self::TransactionVersionNotPermitted(arg0) => f + .debug_tuple("TransactionVersionNotPermitted") + .field(arg0) + .finish(), + Self::TransactionTooLarge => write!(f, "TransactionTooLarge"), + Self::EncodeError(arg0) => f.debug_tuple("EncodeError").field(arg0).finish(), + Self::PrepareError(arg0) => f.debug_tuple("PrepareError").field(arg0).finish(), + Self::SubintentStructureError(arg0, arg1) => f + .debug_tuple("SubintentStructureError") + .field(&arg0.debug_as_display(*context)) + .field(&arg1.debug_as_display(*context)) + .finish(), + Self::IntentValidationError(arg0, arg1) => f + .debug_tuple("IntentValidationError") + .field(&arg0.debug_as_display(*context)) + .field(arg1) + .finish(), + Self::SignatureValidationError(arg0, arg1) => f + .debug_tuple("SignatureValidationError") + .field(&arg0.debug_as_display(*context)) + .field(arg1) + .finish(), + } + } +} + pub enum IntentSpecifier { RootTransactionIntent(TransactionIntentHash), RootSubintent(SubintentHash), @@ -93,6 +128,37 @@ impl TransactionValidationErrorLocation { } } +impl<'a> ContextualDisplay> + for TransactionValidationErrorLocation +{ + type Error = fmt::Error; + + fn contextual_format( + &self, + f: &mut fmt::Formatter, + context: &TransactionHashDisplayContext<'a>, + ) -> Result<(), Self::Error> { + // Copied from the auto-generated `Debug` implementation, and tweaked + match self { + Self::RootTransactionIntent(arg0) => f + .debug_tuple("RootTransactionIntent") + .field(&arg0.debug_as_display(*context)) + .finish(), + Self::RootSubintent(arg0) => f + .debug_tuple("RootSubintent") + .field(&arg0.debug_as_display(*context)) + .finish(), + Self::NonRootSubintent(arg0, arg1) => f + .debug_tuple("NonRootSubintent") + .field(arg0) + .field(&arg1.debug_as_display(*context)) + .finish(), + Self::AcrossTransaction => write!(f, "AcrossTransaction"), + Self::Unlocatable => write!(f, "Unlocatable"), + } + } +} + impl From for TransactionValidationError { fn from(value: PrepareError) -> Self { Self::PrepareError(value) @@ -176,6 +242,33 @@ pub enum SubintentStructureError { MismatchingYieldChildAndYieldParentCountsForSubintent, } +impl<'a> ContextualDisplay> for SubintentStructureError { + type Error = fmt::Error; + + fn contextual_format( + &self, + f: &mut fmt::Formatter, + context: &TransactionHashDisplayContext<'a>, + ) -> Result<(), Self::Error> { + // Copied from the auto-generated `Debug` implementation, and tweaked + match self { + Self::DuplicateSubintent => write!(f, "DuplicateSubintent"), + Self::SubintentHasMultipleParents => write!(f, "SubintentHasMultipleParents"), + Self::ChildSubintentNotIncludedInTransaction(arg0) => f + .debug_tuple("ChildSubintentNotIncludedInTransaction") + .field(&arg0.debug_as_display(*context)) + .finish(), + Self::SubintentExceedsMaxDepth => write!(f, "SubintentExceedsMaxDepth"), + Self::SubintentIsNotReachableFromTheTransactionIntent => { + write!(f, "SubintentIsNotReachableFromTheTransactionIntent") + } + Self::MismatchingYieldChildAndYieldParentCountsForSubintent => { + write!(f, "MismatchingYieldChildAndYieldParentCountsForSubintent") + } + } + } +} + impl SubintentStructureError { pub fn for_unindexed(self) -> TransactionValidationError { TransactionValidationError::SubintentStructureError(