From bdb3281532f289176257c9ff70b9862d15bc3505 Mon Sep 17 00:00:00 2001 From: ruko Date: Thu, 28 Aug 2025 16:59:17 -0700 Subject: [PATCH 01/11] add some tests for cbor decoding --- pallas-primitives/src/conway/model.rs | 119 ++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index 54c1a742a..fba8a5245 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -733,6 +733,125 @@ mod tests { type BlockWrapper<'b> = (u16, Block<'b>); + #[cfg(test)] + mod tests_value { + use super::super::Mint; + use super::super::Multiasset; + use super::super::NonZeroInt; + use super::super::Value; + use pallas_codec::minicbor; + use std::collections::BTreeMap; + + // a value can have zero coins and omit the multiasset + #[test] + fn decode_zero_value() { + let ma: Value = minicbor::decode(&hex::decode("00").unwrap()).unwrap(); + assert_eq!(ma, Value::Coin(0)); + } + + // a value can have zero coins and an empty multiasset map + // Note: this will roundtrip back to "00" + #[test] + fn permit_definite_asset() { + let ma: Value = minicbor::decode(&hex::decode("8200a0").unwrap()).unwrap(); + assert_eq!(ma, Value::Multiasset(0, BTreeMap::new())); + } + + // indefinite-encoded value is invalid + #[test] + fn reject_indefinite_value() { + let ma: Result = minicbor::decode(&hex::decode("9f00a0ff").unwrap()); + assert_eq!( + ma.map_err(|e| e.to_string()), + Err("decode error: Unknown cbor data type for this macro-defined enum.".to_owned()) + ); + } + + // the asset sub-map of a policy map in a multiasset must not be null in Conway + #[test] + fn reject_null_tokens() { + let ma: Result = minicbor::decode(&hex::decode("8200a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap()); + assert_eq!( + ma.map_err(|e| e.to_string()), + Err("bad".to_owned()) + ); + } + + // the asset sub-map of a policy map in a multiasset must not have any zero values in + // Conway + #[test] + fn reject_zero_tokens() { + let ma: Result = minicbor::decode(&hex::decode("8200a1581c00000000000000000000000000000000000000000000000000000000a14000").unwrap()); + assert_eq!( + ma.map_err(|e| e.to_string()), + Err("bad".to_owned()) + ); + } + + #[test] + fn multiasset_reject_null_tokens() { + let ma: Result, _> = minicbor::decode(&hex::decode("a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap()); + assert_eq!( + ma.map_err(|e| e.to_string()), + Err("bad".to_owned()) + ); + } + + // the decoder for MaryValue in the haskell node rejects inputs that are "too big" as + // defined by `isMultiAssetSmallEnough` + #[test] + fn multiasset_not_too_big() { + todo!() + } + + #[test] + fn mint_reject_null_tokens() { + let ma: Result = minicbor::decode(&hex::decode("a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap()); + assert_eq!( + ma.map_err(|e| e.to_string()), + Err("bad".to_owned()) + ); + } + } + + mod tests_transaction { + use super::super::TransactionBody; + use pallas_codec::minicbor; + + // a simple tx with just inputs, outputs, and fee. + // address is all 00 bytes, which seems like it should fail? + #[test] + fn decode_simple_tx() { + let tx_bytes = hex::decode("a300828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a04000000").unwrap(); + let tx: TransactionBody = minicbor::decode(&tx_bytes).unwrap(); + assert_eq!(tx.fee, 0); + } + + // the decoder for ConwayTxBodyRaw rejects transaction bodies missing inputs, outputs, or + // fee + #[test] + fn reject_empty_tx() { + let tx_bytes = hex::decode("a0").unwrap(); + let tx: Result, _> = minicbor::decode(&tx_bytes); + assert_eq!( + tx.map_err(|e| e.to_string()), + Err("bad".to_owned()) + ); + } + + // the mint may not be present if it is empty + #[test] + fn reject_empty_present_mint() { + let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a0400000009a0").unwrap(); + let tx: Result, _> = minicbor::decode(&tx_bytes); + assert_eq!( + tx.map_err(|e| e.to_string()), + Err("bad".to_owned()) + ); + } + + } + #[cfg(test)] mod tests_voter { use super::super::Voter; From 4198d792239ab3e71512fd5d5fa4bd826a3604b9 Mon Sep 17 00:00:00 2001 From: ruko Date: Tue, 9 Sep 2025 06:24:41 -0700 Subject: [PATCH 02/11] add witness set decoder tests --- pallas-primitives/src/conway/model.rs | 98 +++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index fba8a5245..ad185d18e 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -814,6 +814,102 @@ mod tests { } } + mod tests_witness_set { + use super::super::{Bytes, VKeyWitness, WitnessSet}; + use pallas_codec::minicbor; + + #[test] + fn decode_empty_witness_set() { + let witness_set_bytes = hex::decode("a0").unwrap(); + let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); + assert_eq!(ws.vkeywitness, None); + } + + #[test] + fn decode_witness_set_having_vkeywitness_legacy_may_be_empty() { + let witness_set_bytes = hex::decode("a10080").unwrap(); + let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); + + // TODO: It is really surprising that the guard when decoding non + // empty sets from cbor has been commented out + assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![])); + } + + #[test] + fn decode_witness_set_having_vkeywitness_legacy_may_be_indefinite() { + let witness_set_bytes = hex::decode("a1009fff").unwrap(); + let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); + + assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![])); + } + + #[test] + fn decode_witness_set_having_vkeywitness_legacy_singleton() { + let witness_set_bytes = hex::decode("a10081824040").unwrap(); + let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); + + let expected = VKeyWitness { + vkey: Bytes::from(vec![]), + signature: Bytes::from(vec![]), + }; + assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![expected])); + } + + #[test] + fn decode_witness_set_having_vkeywitness_conwaystyle_singleton() { + let witness_set_bytes = hex::decode("a100d9010281824040").unwrap(); + let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); + + let expected = VKeyWitness { + vkey: Bytes::from(vec![]), + signature: Bytes::from(vec![]), + }; + assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![expected])); + } + + #[test] + fn decode_witness_set_having_vkeywitness_conwaystyle_must_be_nonempty() { + let witness_set_bytes = hex::decode("a100d9010280").unwrap(); + let ws: Result = minicbor::decode(&witness_set_bytes); + assert_eq!( + ws.map_err(|e| e.to_string()), + Err("bad".to_owned()) + ); + } + + #[test] + fn decode_witness_set_having_vkeywitness_reject_nonsense_tag() { + // VKey witness set with nonsense tag 259 + let witness_set_bytes = hex::decode("a100d9010381824040").unwrap(); + let ws: Result = minicbor::decode(&witness_set_bytes); + assert_eq!( + ws.map_err(|e| e.to_string()), + Err("decode error: Unrecognised tag: Tag(259)".to_owned()) + ); + } + + // Unclear what the behavior should be when there are duplicates. The haskell code + // allows duplicate entries in the CBOR but represents the vkey witnesses using a + // set data type, so that the resulting data structure will only have one element. + #[test] + fn decode_witness_set_having_vkeywitness_duplicate_entries() { + // VKey witness set with nonsense tag 259 + let witness_set_bytes = hex::decode("a100d9010382824040824040").unwrap(); + let ws: Result = minicbor::decode(&witness_set_bytes); + + let expected = VKeyWitness { + vkey: Bytes::from(vec![]), + signature: Bytes::from(vec![]), + }; + //assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![expected, expected])); + assert_eq!( + ws.map_err(|e| e.to_string()), + Err("bad".to_owned()) + ); + } + + } + mod tests_transaction { use super::super::TransactionBody; use pallas_codec::minicbor; @@ -840,6 +936,8 @@ mod tests { } // the mint may not be present if it is empty + // TODO: equivalent tests for certs, withdrawals, collateral inputs, required signer + // hashes, reference inputs, voting procedures, and proposal procedures #[test] fn reject_empty_present_mint() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a0400000009a0").unwrap(); From a7e55476d91f92412397b70ad5f0bf8aeecf6585 Mon Sep 17 00:00:00 2001 From: ruko Date: Tue, 9 Sep 2025 07:06:42 -0700 Subject: [PATCH 03/11] add aux data tests --- pallas-primitives/src/conway/model.rs | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index ad185d18e..daa33efbf 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -910,6 +910,35 @@ mod tests { } + mod tests_auxdata { + use super::super::PostAlonzoAuxiliaryData; + use pallas_codec::minicbor; + + #[test] + fn decode_auxdata_shelley_format_empty() { + let auxdata_bytes = hex::decode("a0").unwrap(); + let auxdata: PostAlonzoAuxiliaryData = + minicbor::decode(&auxdata_bytes).unwrap(); + assert_eq!(auxdata.metadata, None); + } + + #[test] + fn decode_auxdata_shelley_ma_format_empty() { + let auxdata_bytes = hex::decode("82a080").unwrap(); + let auxdata: PostAlonzoAuxiliaryData = + minicbor::decode(&auxdata_bytes).unwrap(); + assert_eq!(auxdata.metadata, None); + } + + #[test] + fn decode_auxdata_alonzo_format_empty() { + let auxdata_bytes = hex::decode("d90103a0").unwrap(); + let auxdata: PostAlonzoAuxiliaryData = + minicbor::decode(&auxdata_bytes).unwrap(); + assert_eq!(auxdata.metadata, None); + } + } + mod tests_transaction { use super::super::TransactionBody; use pallas_codec::minicbor; From 78adb478e2f7f0fec7d1f88c44bef88379d8c41f Mon Sep 17 00:00:00 2001 From: ruko Date: Tue, 9 Sep 2025 07:27:30 -0700 Subject: [PATCH 04/11] use correct AuxiliaryData type --- pallas-primitives/src/conway/model.rs | 46 +++++++++++++++++++++------ 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index daa33efbf..269fdb62c 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -680,6 +680,13 @@ impl<'b> From> for ScriptRef<'b> { #[deprecated(since = "1.0.0-alpha", note = "use `ScriptRef` instead")] pub type MintedScriptRef<'b> = ScriptRef<'b>; +// FIXME: re-exporting here means it does not use the above PostAlonzoAuxiliaryData; instead, it +// uses the one defined in the alonzo module, which only supports plutus V1 scripts +// +// Same problem exists in the babbage module +// +// should probably take a type parameter for the post-alonzo variant or just define a whole +// separate type here and in babbage pub use crate::alonzo::AuxiliaryData; /// A memory representation of an already minted block @@ -893,8 +900,7 @@ mod tests { // set data type, so that the resulting data structure will only have one element. #[test] fn decode_witness_set_having_vkeywitness_duplicate_entries() { - // VKey witness set with nonsense tag 259 - let witness_set_bytes = hex::decode("a100d9010382824040824040").unwrap(); + let witness_set_bytes = hex::decode("a100d9010282824040824040").unwrap(); let ws: Result = minicbor::decode(&witness_set_bytes); let expected = VKeyWitness { @@ -911,31 +917,53 @@ mod tests { } mod tests_auxdata { - use super::super::PostAlonzoAuxiliaryData; + use super::super::AuxiliaryData; use pallas_codec::minicbor; + use std::collections::BTreeMap; #[test] fn decode_auxdata_shelley_format_empty() { let auxdata_bytes = hex::decode("a0").unwrap(); - let auxdata: PostAlonzoAuxiliaryData = + let auxdata: AuxiliaryData = minicbor::decode(&auxdata_bytes).unwrap(); - assert_eq!(auxdata.metadata, None); + match auxdata { + AuxiliaryData::Shelley(s) => { + assert_eq!(s, BTreeMap::new()); + } + _ => { + panic!("Unexpected variant"); + } + } } #[test] fn decode_auxdata_shelley_ma_format_empty() { let auxdata_bytes = hex::decode("82a080").unwrap(); - let auxdata: PostAlonzoAuxiliaryData = + let auxdata: AuxiliaryData = minicbor::decode(&auxdata_bytes).unwrap(); - assert_eq!(auxdata.metadata, None); + match auxdata { + AuxiliaryData::ShelleyMa(s) => { + assert_eq!(s.transaction_metadata, BTreeMap::new()); + } + _ => { + panic!("Unexpected variant"); + } + } } #[test] fn decode_auxdata_alonzo_format_empty() { let auxdata_bytes = hex::decode("d90103a0").unwrap(); - let auxdata: PostAlonzoAuxiliaryData = + let auxdata: AuxiliaryData = minicbor::decode(&auxdata_bytes).unwrap(); - assert_eq!(auxdata.metadata, None); + match auxdata { + AuxiliaryData::PostAlonzo(a) => { + assert_eq!(a.metadata, None); + } + _ => { + panic!("Unexpected variant"); + } + } } } From c5ba3cd3586564ca0a4f307aec32abc42fa72d9d Mon Sep 17 00:00:00 2001 From: ruko Date: Wed, 10 Sep 2025 08:24:06 -0700 Subject: [PATCH 05/11] fix some tests --- pallas-primitives/src/conway/model.rs | 64 ++++++++++++++++++++------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index 269fdb62c..28644991e 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -733,6 +733,20 @@ pub struct Tx<'b> { #[deprecated(since = "1.0.0-alpha", note = "use `Tx` instead")] pub type MintedTx<'b> = Tx<'b>; +fn is_multiasset_small_enough(ma: Multiasset) -> bool { + let per_asset_size = 44; + let per_policy_size = 28; + + let policy_count = ma.len(); + let mut asset_count = 0; + for (policy, assets) in ma { + asset_count += assets.len(); + } + + let size = per_asset_size * asset_count + per_policy_size * policy_count; + size <= 65535 +} + #[cfg(test)] mod tests { use super::Block; @@ -759,19 +773,16 @@ mod tests { // a value can have zero coins and an empty multiasset map // Note: this will roundtrip back to "00" #[test] - fn permit_definite_asset() { + fn permit_definite_value() { let ma: Value = minicbor::decode(&hex::decode("8200a0").unwrap()).unwrap(); assert_eq!(ma, Value::Multiasset(0, BTreeMap::new())); } - // indefinite-encoded value is invalid + // Indefinite-encoded value is valid #[test] - fn reject_indefinite_value() { - let ma: Result = minicbor::decode(&hex::decode("9f00a0ff").unwrap()); - assert_eq!( - ma.map_err(|e| e.to_string()), - Err("decode error: Unknown cbor data type for this macro-defined enum.".to_owned()) - ); + fn permit_indefinite_value() { + let ma: Value = minicbor::decode(&hex::decode("9f00a0ff").unwrap()).unwrap(); + assert_eq!(ma, Value::Multiasset(0, BTreeMap::new())); } // the asset sub-map of a policy map in a multiasset must not be null in Conway @@ -808,7 +819,21 @@ mod tests { // defined by `isMultiAssetSmallEnough` #[test] fn multiasset_not_too_big() { - todo!() + // Creating CBOR representation of a value with 1500 policies + // 1500 * 44 is greater than 65535 so this should fail to decode + let mut s: String = "b905dc".to_owned(); + for i in 0..1500u16 { + // policy + s += "581c0000000000000000000000000000000000000000000000000000"; + s += &hex::encode(i.to_be_bytes()); + // minimal token map (conway requires nonempty asset maps) + s += "a14001"; + } + let ma: Result, _> = minicbor::decode(&hex::decode(s).unwrap()); + match ma { + Ok(_) => panic!("decode succeded but should fail"), + Err(e) => assert_eq!(e.to_string(), "bad") + } } #[test] @@ -837,8 +862,15 @@ mod tests { let witness_set_bytes = hex::decode("a10080").unwrap(); let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); - // TODO: It is really surprising that the guard when decoding non - // empty sets from cbor has been commented out + // FIXME: The decoder behavior here is strictly correct w.r.t. the haskell code; we + // must accept a vkeywitness set that is present but empty (in the legacy witness set + // format). + // + // However, the types we are using in pallas here are confusing; vkeywitness is of type + // Option, and in fact, our "NonEmptySet" type allows constructing an + // empty value via CBOR decoding (there used to be a guard, but it was commented out). + // So we end up with a Some(vec![]). It would make more sense to just have a 'Set' + // type. assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![])); } @@ -898,6 +930,8 @@ mod tests { // Unclear what the behavior should be when there are duplicates. The haskell code // allows duplicate entries in the CBOR but represents the vkey witnesses using a // set data type, so that the resulting data structure will only have one element. + // However, our NonEmptySet type is secretly a vector and does not prevent duplicates. + // Do we ever hash witness sets? i.e. do we need to remember the original bytes? #[test] fn decode_witness_set_having_vkeywitness_duplicate_entries() { let witness_set_bytes = hex::decode("a100d9010282824040824040").unwrap(); @@ -971,8 +1005,8 @@ mod tests { use super::super::TransactionBody; use pallas_codec::minicbor; - // a simple tx with just inputs, outputs, and fee. - // address is all 00 bytes, which seems like it should fail? + // A simple tx with just inputs, outputs, and fee. Address is not well-formed, since the + // 00 header implies both a payment part and a staking part are present. #[test] fn decode_simple_tx() { let tx_bytes = hex::decode("a300828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a04000000").unwrap(); @@ -980,7 +1014,7 @@ mod tests { assert_eq!(tx.fee, 0); } - // the decoder for ConwayTxBodyRaw rejects transaction bodies missing inputs, outputs, or + // The decoder for ConwayTxBodyRaw rejects transaction bodies missing inputs, outputs, or // fee #[test] fn reject_empty_tx() { @@ -992,7 +1026,7 @@ mod tests { ); } - // the mint may not be present if it is empty + // The mint may not be present if it is empty // TODO: equivalent tests for certs, withdrawals, collateral inputs, required signer // hashes, reference inputs, voting procedures, and proposal procedures #[test] From deecbc13bcecab0365ee1c2457555d23cc192b2c Mon Sep 17 00:00:00 2001 From: ruko Date: Thu, 11 Sep 2025 06:01:01 -0700 Subject: [PATCH 06/11] transaction body decoder tests --- pallas-codec/src/utils.rs | 19 +- pallas-primitives/src/conway/model.rs | 472 +++++++++++++++++++++++--- 2 files changed, 440 insertions(+), 51 deletions(-) diff --git a/pallas-codec/src/utils.rs b/pallas-codec/src/utils.rs index cb2eec8c1..988e1966f 100644 --- a/pallas-codec/src/utils.rs +++ b/pallas-codec/src/utils.rs @@ -858,9 +858,9 @@ where let inner: Vec = d.decode_with(ctx)?; - // if inner.is_empty() { - // return Err(Error::message("decoding empty set as NonEmptySet")); - // } + if inner.is_empty() { + return Err(Error::message("decoding empty set as NonEmptySet")); + } Ok(Self(inner)) } @@ -998,7 +998,7 @@ impl From<&AnyUInt> for u64 { /// Introduced in Conway /// positive_coin = 1 .. 18446744073709551615 #[derive( - Encode, Decode, Debug, PartialEq, Copy, Clone, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize, + Encode, Debug, PartialEq, Copy, Clone, PartialOrd, Eq, Ord, Hash, Serialize, Deserialize, )] #[serde(transparent)] #[cbor(transparent)] @@ -1016,6 +1016,17 @@ impl TryFrom for PositiveCoin { } } +impl<'b, C> minicbor::Decode<'b, C> for PositiveCoin { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + let n = d.decode_with(ctx)?; + if n == 0 { + return Err(minicbor::decode::Error::message("PositiveCoin must not be 0")); + } + Ok(PositiveCoin(n)) + } +} + + impl From for u64 { fn from(value: PositiveCoin) -> Self { value.0 diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index 28644991e..aec2f1ef6 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -27,7 +27,33 @@ pub use crate::babbage::OperationalCert; pub use crate::babbage::Header; -pub type Multiasset = BTreeMap>; +use pallas_codec::minicbor::data::Type; + +use std::collections::HashSet; + +#[derive(Serialize, Deserialize, Encode, Debug, PartialEq, Eq, Clone)] +pub struct Multiasset(#[n(0)] BTreeMap>); + +impl<'b, C, A: minicbor::Decode<'b, C>> minicbor::Decode<'b, C> for Multiasset { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + let policies: BTreeMap> = d.decode_with(ctx)?; + + // In Conway, all policies must be nonempty, and all amounts must be nonzero. + // We always parameterize Multiasset with NonZeroInt in practice, but maybe it should be + // monomorphic? + for (_policy, assets) in &policies { + if assets.len() == 0 { + return Err(minicbor::decode::Error::message("Policy must not be empty")); + } + } + + let result = Multiasset(policies); + if !is_multiasset_small_enough(&result) { + return Err(minicbor::decode::Error::message("Multiasset must not exceed size limit")); + } + Ok(result) + } +} pub type Mint = Multiasset; @@ -37,10 +63,50 @@ pub enum Value { Multiasset(Coin, Multiasset), } -codec_by_datatype! { - Value, - U8 | U16 | U32 | U64 => Coin, - (coin, multi => Multiasset) +//codec_by_datatype! { +// Value, +// U8 | U16 | U32 | U64 => Coin, +// (coin, multi => Multiasset) +//} + +impl minicbor::Encode for Value { + fn encode( + &self, + e: &mut minicbor::Encoder, + _ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + match self { + Value::Coin(coin) => { + e.encode(coin)?; + }, + Value::Multiasset(coin, ma) => { + e.array(2)?; + e.encode(coin)?; + e.encode(ma)?; + } + } + Ok(()) + } +} + +impl<'b, C> minicbor::Decode<'b, C> for Value { + fn decode(d: &mut minicbor::Decoder<'b>, _ctx: &mut C) -> Result { + match d.datatype()? { + Type::U8 | Type::U16 | Type::U32 | Type::U64 => { + let coin = d.decode()?; + Ok(Value::Coin(coin)) + } + Type::Array | Type::ArrayIndef => { + let _ = d.array()?; + let coin = d.decode()?; + let multiasset = d.decode()?; + Ok(Value::Multiasset(coin, multiasset)) + } + t => { + Err(minicbor::decode::Error::message(format!("Unexpected datatype {}", t))) + } + } + } } pub use crate::alonzo::TransactionOutput as LegacyTransactionOutput; @@ -311,7 +377,7 @@ pub struct DRepVotingThresholds { pub treasury_withdrawal: UnitInterval, } -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)] +#[derive(Serialize, Deserialize, Encode, Debug, PartialEq, Clone)] #[cbor(map)] pub struct TransactionBody<'a> { #[n(0)] @@ -376,6 +442,183 @@ pub struct TransactionBody<'a> { pub donation: Option, } +// This is ugly but I'm not sure how to do it with minicbor-derive +// the cbor map implementation is here: +// https://github.com/twittner/minicbor/blob/83a4a0f868ac9ffc924a282f8f917aa2ad7c698a/minicbor-derive/src/decode.rs#L405-L424 +// We need to do validation inside the decoder or change the types of the validated fields to +// new types that do their own validation +impl<'b, C> minicbor::Decode<'b, C> for TransactionBody<'b> { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + let mut must_inputs = None; + let mut must_outputs = None; + let mut must_fee = None; + let mut ttl = None; + let mut certificates = None; + let mut withdrawals = None; + let mut auxiliary_data_hash = None; + let mut validity_interval_start = None; + let mut mint: Option> = None; + let mut script_data_hash = None; + let mut collateral = None; + let mut required_signers = None; + let mut network_id = None; + let mut collateral_return = None; + let mut total_collateral = None; + let mut reference_inputs = None; + let mut voting_procedures = None; + let mut proposal_procedures = None; + let mut treasury_value = None; + let mut donation = None; + + let map_init = d.map()?; + let mut items_seen = 0; + + let mut seen_key = HashSet::new(); + + loop { + let n = d.i64(); + let Ok(index) = n else { break }; + if seen_key.contains(&index) { + return Err(minicbor::decode::Error::message("transaction body must not contain duplicate keys")); + } + match index { + 0 => { + must_inputs = d.decode_with(ctx)?; + }, + 1 => { + must_outputs = d.decode_with(ctx)?; + }, + 2 => { + must_fee = d.decode_with(ctx)?; + }, + 3 => { + ttl = d.decode_with(ctx)?; + }, + 4 => { + certificates = d.decode_with(ctx)?; + }, + 5 => { + let real_withdrawals: BTreeMap = d.decode_with(ctx)?; + if real_withdrawals.len() == 0 { + return Err(minicbor::decode::Error::message("withdrawals must be non-empty if present")); + } + withdrawals = Some(real_withdrawals); + }, + 7 => { + auxiliary_data_hash = d.decode_with(ctx)?; + }, + 8 => { + validity_interval_start = d.decode_with(ctx)?; + }, + 9 => { + let real_mint: Multiasset = d.decode_with(ctx)?; + if real_mint.0.len() == 0 { + return Err(minicbor::decode::Error::message("mint must be non-empty if present")); + } + mint = Some(real_mint); + }, + 11 => { + script_data_hash = d.decode_with(ctx)?; + }, + 13 => { + collateral = d.decode_with(ctx)?; + }, + 14 => { + required_signers = d.decode_with(ctx)?; + }, + 15 => { + network_id = d.decode_with(ctx)?; + }, + 16 => { + collateral_return = d.decode_with(ctx)?; + }, + 17 => { + total_collateral = d.decode_with(ctx)?; + }, + 18 => { + reference_inputs = d.decode_with(ctx)?; + }, + 19 => { + let real_voting_procedures: VotingProcedures = d.decode_with(ctx)?; + if real_voting_procedures.len() == 0 { + return Err(minicbor::decode::Error::message("voting procedures must be non-empty if present")); + } + voting_procedures = Some(real_voting_procedures); + }, + 20 => { + let real_proposal_procedures: NonEmptySet = d.decode_with(ctx)?; + if real_proposal_procedures.len() == 0 { + return Err(minicbor::decode::Error::message("proposal procedures must be non-empty if present")); + } + proposal_procedures = Some(real_proposal_procedures); + }, + 21 => { + treasury_value = d.decode_with(ctx)?; + }, + 22 => { + donation = d.decode_with(ctx)?; + }, + _ => { + return Err(minicbor::decode::Error::message("unexpected index")); + } + } + seen_key.insert(index); + items_seen += 1; + if let Some(map_count) = map_init { + if items_seen == map_count { + break; + } + } + } + + if let Some(map_count) = map_init { + if map_count != items_seen { + return Err(minicbor::decode::Error::message("map is not valid cbor: declared count did not match actual count")); + } + } else { + let ty = d.datatype()?; + if ty == minicbor::data::Type::Break { + d.skip()?; + } else { + return Err(minicbor::decode::Error::message("unexpected garbage at end of map")); + } + } + + let Some(inputs) = must_inputs else { + return Err(minicbor::decode::Error::message("field inputs is required")); + }; + let Some(outputs) = must_outputs else { + return Err(minicbor::decode::Error::message("field outputs is required")); + }; + let Some(fee) = must_fee else { + return Err(minicbor::decode::Error::message("field fee is required")); + }; + + Ok(Self { + inputs, + outputs, + fee, + ttl, + certificates, + withdrawals, + auxiliary_data_hash, + validity_interval_start, + mint, + script_data_hash, + collateral, + required_signers, + network_id, + collateral_return, + total_collateral, + reference_inputs, + voting_procedures, + proposal_procedures, + treasury_value, + donation, + }) + } +} + #[deprecated(since = "1.0.0-alpha", note = "use `TransactionBody` instead")] pub type MintedTransactionBody<'a> = TransactionBody<'a>; @@ -616,6 +859,19 @@ pub struct WitnessSet<'b> { pub plutus_v3_script: Option>>, } +//impl<'b, C> minicbor::Decode<'b, C> for WitnessSet<'b> { +// fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { +// let vkeywitness = d.decode_with(ctx)?; +// let native_script = d.decode_with(ctx)?; +// let bootstrap_witness = d.decode_with(ctx)?; +// let plutus_v1_script = d.decode_with(ctx)?; +// let plutus_data = d.decode_with(ctx)?; +// let redeemer = d.decode_with(ctx)?; +// let plutus_v2_script = d.decode_with(ctx)?; +// let plutus_v3_script = d.decode_with(ctx)?; +// } + + #[deprecated(since = "1.0.0-alpha", note = "use `WitnessSet` instead")] pub type MintedWitnessSet<'b> = WitnessSet<'b>; @@ -733,13 +989,13 @@ pub struct Tx<'b> { #[deprecated(since = "1.0.0-alpha", note = "use `Tx` instead")] pub type MintedTx<'b> = Tx<'b>; -fn is_multiasset_small_enough(ma: Multiasset) -> bool { +fn is_multiasset_small_enough(ma: &Multiasset) -> bool { let per_asset_size = 44; let per_policy_size = 28; - let policy_count = ma.len(); + let policy_count = ma.0.len(); let mut asset_count = 0; - for (policy, assets) in ma { + for (_policy, assets) in &ma.0 { asset_count += assets.len(); } @@ -775,14 +1031,14 @@ mod tests { #[test] fn permit_definite_value() { let ma: Value = minicbor::decode(&hex::decode("8200a0").unwrap()).unwrap(); - assert_eq!(ma, Value::Multiasset(0, BTreeMap::new())); + assert_eq!(ma, Value::Multiasset(0, Multiasset(BTreeMap::new()))); } // Indefinite-encoded value is valid #[test] fn permit_indefinite_value() { let ma: Value = minicbor::decode(&hex::decode("9f00a0ff").unwrap()).unwrap(); - assert_eq!(ma, Value::Multiasset(0, BTreeMap::new())); + assert_eq!(ma, Value::Multiasset(0, Multiasset(BTreeMap::new()))); } // the asset sub-map of a policy map in a multiasset must not be null in Conway @@ -791,7 +1047,7 @@ mod tests { let ma: Result = minicbor::decode(&hex::decode("8200a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap()); assert_eq!( ma.map_err(|e| e.to_string()), - Err("bad".to_owned()) + Err("decode error: Policy must not be empty".to_owned()) ); } @@ -802,7 +1058,7 @@ mod tests { let ma: Result = minicbor::decode(&hex::decode("8200a1581c00000000000000000000000000000000000000000000000000000000a14000").unwrap()); assert_eq!( ma.map_err(|e| e.to_string()), - Err("bad".to_owned()) + Err("decode error: PositiveCoin must not be 0".to_owned()) ); } @@ -811,7 +1067,7 @@ mod tests { let ma: Result, _> = minicbor::decode(&hex::decode("a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap()); assert_eq!( ma.map_err(|e| e.to_string()), - Err("bad".to_owned()) + Err("decode error: Policy must not be empty".to_owned()) ); } @@ -832,7 +1088,7 @@ mod tests { let ma: Result, _> = minicbor::decode(&hex::decode(s).unwrap()); match ma { Ok(_) => panic!("decode succeded but should fail"), - Err(e) => assert_eq!(e.to_string(), "bad") + Err(e) => assert_eq!(e.to_string(), "decode error: Multiasset must not exceed size limit") } } @@ -841,7 +1097,7 @@ mod tests { let ma: Result = minicbor::decode(&hex::decode("a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap()); assert_eq!( ma.map_err(|e| e.to_string()), - Err("bad".to_owned()) + Err("decode error: Policy must not be empty".to_owned()) ); } } @@ -857,33 +1113,57 @@ mod tests { assert_eq!(ws.vkeywitness, None); } - #[test] - fn decode_witness_set_having_vkeywitness_legacy_may_be_empty() { - let witness_set_bytes = hex::decode("a10080").unwrap(); - let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); - - // FIXME: The decoder behavior here is strictly correct w.r.t. the haskell code; we - // must accept a vkeywitness set that is present but empty (in the legacy witness set - // format). - // - // However, the types we are using in pallas here are confusing; vkeywitness is of type - // Option, and in fact, our "NonEmptySet" type allows constructing an - // empty value via CBOR decoding (there used to be a guard, but it was commented out). - // So we end up with a Some(vec![]). It would make more sense to just have a 'Set' - // type. - assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![])); - } + // Legacy format is not supported when decoder version is 9 + // These tests should go in a pre-conway module? + //#[test] + //fn decode_witness_set_having_vkeywitness_legacy_may_be_empty() { + // let witness_set_bytes = hex::decode("a10080").unwrap(); + // let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); + + // // FIXME: The decoder behavior here is strictly correct w.r.t. the haskell code; we + // // must accept a vkeywitness set that is present but empty (in the legacy witness set + // // format). + // // + // // However, the types we are using in pallas here are confusing; vkeywitness is of type + // // Option, and in fact, our "NonEmptySet" type allows constructing an + // // empty value via CBOR decoding (there used to be a guard, but it was commented out). + // // So we end up with a Some(vec![]). It would make more sense to just have a 'Set' + // // type. + // assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![])); + //} + + //#[test] + //fn decode_witness_set_having_vkeywitness_legacy_may_be_indefinite() { + // let witness_set_bytes = hex::decode("a1009fff").unwrap(); + // let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); + + // assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![])); + //} + + //#[test] + //fn decode_witness_set_having_vkeywitness_legacy_singleton() { + // let witness_set_bytes = hex::decode("a10081824040").unwrap(); + // let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); + + // let expected = VKeyWitness { + // vkey: Bytes::from(vec![]), + // signature: Bytes::from(vec![]), + // }; + // assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![expected])); + //} #[test] - fn decode_witness_set_having_vkeywitness_legacy_may_be_indefinite() { - let witness_set_bytes = hex::decode("a1009fff").unwrap(); - let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); - - assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![])); + fn decode_witness_set_having_vkeywitness_untagged_must_be_nonempty() { + let witness_set_bytes = hex::decode("a10080").unwrap(); + let ws: Result = minicbor::decode(&witness_set_bytes); + assert_eq!( + ws.map_err(|e| e.to_string()), + Err("decode error: decoding empty set as NonEmptySet".to_owned()) + ); } #[test] - fn decode_witness_set_having_vkeywitness_legacy_singleton() { + fn decode_witness_set_having_vkeywitness_untagged_singleton() { let witness_set_bytes = hex::decode("a10081824040").unwrap(); let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); @@ -912,7 +1192,7 @@ mod tests { let ws: Result = minicbor::decode(&witness_set_bytes); assert_eq!( ws.map_err(|e| e.to_string()), - Err("bad".to_owned()) + Err("decode error: decoding empty set as NonEmptySet".to_owned()) ); } @@ -935,17 +1215,13 @@ mod tests { #[test] fn decode_witness_set_having_vkeywitness_duplicate_entries() { let witness_set_bytes = hex::decode("a100d9010282824040824040").unwrap(); - let ws: Result = minicbor::decode(&witness_set_bytes); + let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); let expected = VKeyWitness { vkey: Bytes::from(vec![]), signature: Bytes::from(vec![]), }; - //assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![expected, expected])); - assert_eq!( - ws.map_err(|e| e.to_string()), - Err("bad".to_owned()) - ); + assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![expected.clone(), expected])); } } @@ -1022,7 +1298,29 @@ mod tests { let tx: Result, _> = minicbor::decode(&tx_bytes); assert_eq!( tx.map_err(|e| e.to_string()), - Err("bad".to_owned()) + Err("decode error: field inputs is required".to_owned()) + ); + } + + // Single input, no outputs, fee present but zero + #[test] + fn reject_tx_missing_outputs() { + let tx_bytes = hex::decode("a200818258200000000000000000000000000000000000000000000000000000000000000008090200").unwrap(); + let tx: Result, _> = minicbor::decode(&tx_bytes); + assert_eq!( + tx.map_err(|e| e.to_string()), + Err("decode error: field outputs is required".to_owned()) + ); + } + + // Single input, single output, no fee + #[test] + fn reject_tx_missing_fee() { + let tx_bytes = hex::decode("a20081825820000000000000000000000000000000000000000000000000000000000000000809018182581c000000000000000000000000000000000000000000000000000000001affffffff").unwrap(); + let tx: Result, _> = minicbor::decode(&tx_bytes); + assert_eq!( + tx.map_err(|e| e.to_string()), + Err("decode error: field fee is required".to_owned()) ); } @@ -1035,10 +1333,90 @@ mod tests { let tx: Result, _> = minicbor::decode(&tx_bytes); assert_eq!( tx.map_err(|e| e.to_string()), - Err("bad".to_owned()) + Err("decode error: mint must be non-empty if present".to_owned()) ); } + #[test] + fn reject_empty_present_certs() { + let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000000480").unwrap(); + let tx: Result, _> = minicbor::decode(&tx_bytes); + assert_eq!( + tx.map_err(|e| e.to_string()), + Err("decode error: decoding empty set as NonEmptySet".to_owned()) + ); + } + + #[test] + fn reject_empty_present_withdrawals() { + let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a0400000005a0").unwrap(); + let tx: Result, _> = minicbor::decode(&tx_bytes); + assert_eq!( + tx.map_err(|e| e.to_string()), + Err("decode error: withdrawals must be non-empty if present".to_owned()) + ); + } + + #[test] + fn reject_empty_present_collateral_inputs() { + let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000000d80").unwrap(); + let tx: Result, _> = minicbor::decode(&tx_bytes); + assert_eq!( + tx.map_err(|e| e.to_string()), + Err("decode error: decoding empty set as NonEmptySet".to_owned()) + ); + } + + #[test] + fn reject_empty_present_required_signers() { + let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000000e80").unwrap(); + let tx: Result, _> = minicbor::decode(&tx_bytes); + assert_eq!( + tx.map_err(|e| e.to_string()), + Err("decode error: decoding empty set as NonEmptySet".to_owned()) + ); + } + + #[test] + fn reject_empty_present_voting_procedures() { + let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a0400000013a0").unwrap(); + let tx: Result, _> = minicbor::decode(&tx_bytes); + assert_eq!( + tx.map_err(|e| e.to_string()), + Err("decode error: voting procedures must be non-empty if present".to_owned()) + ); + } + + #[test] + fn reject_empty_present_proposal_procedures() { + let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000001480").unwrap(); + let tx: Result, _> = minicbor::decode(&tx_bytes); + assert_eq!( + tx.map_err(|e| e.to_string()), + Err("decode error: decoding empty set as NonEmptySet".to_owned()) + ); + } + + #[test] + fn reject_empty_present_donation() { + let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000001600").unwrap(); + let tx: Result, _> = minicbor::decode(&tx_bytes); + assert_eq!( + tx.map_err(|e| e.to_string()), + Err("decode error: PositiveCoin must not be 0".to_owned()) + ); + } + + + #[test] + fn reject_duplicate_keys() { + let tx_bytes = hex::decode("a40081825820000000000000000000000000000000000000000000000000000000000000000809018182581c000000000000000000000000000000000000000000000000000000001affffffff02010201").unwrap(); + let tx: Result, _> = minicbor::decode(&tx_bytes); + assert_eq!( + tx.map_err(|e| e.to_string()), + Err("decode error: transaction body must not contain duplicate keys".to_owned()) + ); + } } #[cfg(test)] From 7a1a9a49c348da79b003848ec5de0aa0364408c9 Mon Sep 17 00:00:00 2001 From: ruko Date: Thu, 11 Sep 2025 06:46:01 -0700 Subject: [PATCH 07/11] delete commented code --- pallas-primitives/src/conway/model.rs | 58 --------------------------- 1 file changed, 58 deletions(-) diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index aec2f1ef6..c6b5faffe 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -63,12 +63,6 @@ pub enum Value { Multiasset(Coin, Multiasset), } -//codec_by_datatype! { -// Value, -// U8 | U16 | U32 | U64 => Coin, -// (coin, multi => Multiasset) -//} - impl minicbor::Encode for Value { fn encode( &self, @@ -859,19 +853,6 @@ pub struct WitnessSet<'b> { pub plutus_v3_script: Option>>, } -//impl<'b, C> minicbor::Decode<'b, C> for WitnessSet<'b> { -// fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { -// let vkeywitness = d.decode_with(ctx)?; -// let native_script = d.decode_with(ctx)?; -// let bootstrap_witness = d.decode_with(ctx)?; -// let plutus_v1_script = d.decode_with(ctx)?; -// let plutus_data = d.decode_with(ctx)?; -// let redeemer = d.decode_with(ctx)?; -// let plutus_v2_script = d.decode_with(ctx)?; -// let plutus_v3_script = d.decode_with(ctx)?; -// } - - #[deprecated(since = "1.0.0-alpha", note = "use `WitnessSet` instead")] pub type MintedWitnessSet<'b> = WitnessSet<'b>; @@ -1113,45 +1094,6 @@ mod tests { assert_eq!(ws.vkeywitness, None); } - // Legacy format is not supported when decoder version is 9 - // These tests should go in a pre-conway module? - //#[test] - //fn decode_witness_set_having_vkeywitness_legacy_may_be_empty() { - // let witness_set_bytes = hex::decode("a10080").unwrap(); - // let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); - - // // FIXME: The decoder behavior here is strictly correct w.r.t. the haskell code; we - // // must accept a vkeywitness set that is present but empty (in the legacy witness set - // // format). - // // - // // However, the types we are using in pallas here are confusing; vkeywitness is of type - // // Option, and in fact, our "NonEmptySet" type allows constructing an - // // empty value via CBOR decoding (there used to be a guard, but it was commented out). - // // So we end up with a Some(vec![]). It would make more sense to just have a 'Set' - // // type. - // assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![])); - //} - - //#[test] - //fn decode_witness_set_having_vkeywitness_legacy_may_be_indefinite() { - // let witness_set_bytes = hex::decode("a1009fff").unwrap(); - // let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); - - // assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![])); - //} - - //#[test] - //fn decode_witness_set_having_vkeywitness_legacy_singleton() { - // let witness_set_bytes = hex::decode("a10081824040").unwrap(); - // let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); - - // let expected = VKeyWitness { - // vkey: Bytes::from(vec![]), - // signature: Bytes::from(vec![]), - // }; - // assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![expected])); - //} - #[test] fn decode_witness_set_having_vkeywitness_untagged_must_be_nonempty() { let witness_set_bytes = hex::decode("a10080").unwrap(); From 4f85d34a7d2dc6ccd462e3e007c8297b42308d73 Mon Sep 17 00:00:00 2001 From: ruko Date: Mon, 15 Sep 2025 12:26:16 -0700 Subject: [PATCH 08/11] use is_some_and --- pallas-primitives/src/conway/model.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index c6b5faffe..b3182d611 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -565,11 +565,11 @@ impl<'b, C> minicbor::Decode<'b, C> for TransactionBody<'b> { } } - if let Some(map_count) = map_init { - if map_count != items_seen { - return Err(minicbor::decode::Error::message("map is not valid cbor: declared count did not match actual count")); - } - } else { + if map_init.is_some_and(|map_count| map_count != items_seen) { + return Err(minicbor::decode::Error::message("map is not valid cbor: declared count did not match actual count")); + } + + if map_init.is_none() { let ty = d.datatype()?; if ty == minicbor::data::Type::Break { d.skip()?; From 8d2fbe2bfc3dbb133c57db9910f13d5ba1122172 Mon Sep 17 00:00:00 2001 From: ruko Date: Mon, 15 Sep 2025 12:57:15 -0700 Subject: [PATCH 09/11] make multiasset wrapper transparent to fix pallas-traverse build --- pallas-primitives/src/conway/model.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index b3182d611..d47fd115e 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -34,6 +34,27 @@ use std::collections::HashSet; #[derive(Serialize, Deserialize, Encode, Debug, PartialEq, Eq, Clone)] pub struct Multiasset(#[n(0)] BTreeMap>); +impl From<[(PolicyId, BTreeMap); N]> for Multiasset + where BTreeMap>: From<[(PolicyId, BTreeMap); N]> { + fn from(x: [(PolicyId, BTreeMap); N]) -> Self { + Multiasset(BTreeMap::>::from(x)) + } +} + +impl FromIterator<(PolicyId, BTreeMap)> for Multiasset { + fn from_iter)>>(iter: I) -> Self { + Multiasset(BTreeMap::>::from_iter(iter)) + } +} + +impl std::ops::Deref for Multiasset { + type Target = BTreeMap>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl<'b, C, A: minicbor::Decode<'b, C>> minicbor::Decode<'b, C> for Multiasset { fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { let policies: BTreeMap> = d.decode_with(ctx)?; From 937f2cf64a9d99baf6127d6cb5bab4fefd41c017 Mon Sep 17 00:00:00 2001 From: ruko Date: Mon, 13 Oct 2025 13:50:56 -0700 Subject: [PATCH 10/11] implement pedantic validations as minicbor context --- pallas-primitives/src/conway/model.rs | 880 +++++++++++++++++++------- pallas-txbuilder/src/conway.rs | 10 +- pallas-validate/src/phase1/conway.rs | 2 +- 3 files changed, 647 insertions(+), 245 deletions(-) diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index d47fd115e..92b4d724e 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -29,7 +29,39 @@ pub use crate::babbage::Header; use pallas_codec::minicbor::data::Type; -use std::collections::HashSet; +trait StrictContext { + fn push_error(&mut self, s: String); + fn get_errors(&self) -> &[String]; +} + +pub struct BasicStrictContext { + errors: Vec, +} + +impl BasicStrictContext { + pub fn new() -> Self { + BasicStrictContext { + errors: vec![] + } + } +} + +impl StrictContext for BasicStrictContext { + fn push_error(&mut self, s: String) { + self.errors.push(s) + } + fn get_errors(&self) -> &[String] { + &self.errors + } +} + +impl StrictContext for () { + fn push_error(&mut self, _s: String) { + } + fn get_errors(&self) -> &[String] { + &[] + } +} #[derive(Serialize, Deserialize, Encode, Debug, PartialEq, Eq, Clone)] pub struct Multiasset(#[n(0)] BTreeMap>); @@ -55,28 +87,79 @@ impl std::ops::Deref for Multiasset { } } -impl<'b, C, A: minicbor::Decode<'b, C>> minicbor::Decode<'b, C> for Multiasset { - fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { +impl<'b, Ctx, A: minicbor::Decode<'b, Ctx>> minicbor::Decode<'b, Ctx> for Multiasset +where + Ctx: StrictContext +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { let policies: BTreeMap> = d.decode_with(ctx)?; // In Conway, all policies must be nonempty, and all amounts must be nonzero. // We always parameterize Multiasset with NonZeroInt in practice, but maybe it should be // monomorphic? - for (_policy, assets) in &policies { - if assets.len() == 0 { - return Err(minicbor::decode::Error::message("Policy must not be empty")); + for assets in policies.values() { + if assets.is_empty() { + ctx.push_error("Policy must not be empty".to_string()); } } let result = Multiasset(policies); if !is_multiasset_small_enough(&result) { - return Err(minicbor::decode::Error::message("Multiasset must not exceed size limit")); + ctx.push_error("Multiasset must not exceed size limit".to_string()); } Ok(result) } } -pub type Mint = Multiasset; +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct StrictMultiasset { + inner: Multiasset +} + +impl<'b, Ctx, A: minicbor::Decode<'b, Ctx>> minicbor::Decode<'b, Ctx> for StrictMultiasset +where + Ctx: StrictContext +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { + let inner: Multiasset = d.decode_with(ctx)?; + let errs = ctx.get_errors(); + if !errs.is_empty() { + let s = errs.join(";"); + return Err(minicbor::decode::Error::message( + format!("Multiasset failed strict validations: {}", s) + )); + } + Ok(StrictMultiasset { + inner + }) + } +} + +pub type Mint = NonEmptyMultiasset; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct StrictMint { + inner: Mint +} + +impl<'b, Ctx> minicbor::Decode<'b, Ctx> for StrictMint +where + Ctx: StrictContext +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { + let inner: Mint = d.decode_with(ctx)?; + let errs = ctx.get_errors(); + if !errs.is_empty() { + let s = errs.join(";"); + return Err(minicbor::decode::Error::message( + format!("Mint failed strict validations: {}", s) + )); + } + Ok(StrictMint { + inner + }) + } +} #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub enum Value { @@ -104,17 +187,20 @@ impl minicbor::Encode for Value { } } -impl<'b, C> minicbor::Decode<'b, C> for Value { - fn decode(d: &mut minicbor::Decoder<'b>, _ctx: &mut C) -> Result { +impl<'b, Ctx> minicbor::Decode<'b, Ctx> for Value +where + Ctx: StrictContext +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { match d.datatype()? { Type::U8 | Type::U16 | Type::U32 | Type::U64 => { - let coin = d.decode()?; + let coin = d.decode_with(ctx)?; Ok(Value::Coin(coin)) } Type::Array | Type::ArrayIndef => { let _ = d.array()?; - let coin = d.decode()?; - let multiasset = d.decode()?; + let coin = d.decode_with(ctx)?; + let multiasset = d.decode_with(ctx)?; Ok(Value::Multiasset(coin, multiasset)) } t => { @@ -124,9 +210,33 @@ impl<'b, C> minicbor::Decode<'b, C> for Value { } } +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct StrictValue { + inner: Value +} + +impl<'b, Ctx> minicbor::Decode<'b, Ctx> for StrictValue +where + Ctx: StrictContext +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { + let inner: Value = d.decode_with(ctx)?; + let errs = ctx.get_errors(); + if !errs.is_empty() { + let s = errs.join(";"); + return Err(minicbor::decode::Error::message( + format!("Value failed strict validations: {}", s) + )); + } + Ok(StrictValue { + inner + }) + } +} + pub use crate::alonzo::TransactionOutput as LegacyTransactionOutput; -pub type Withdrawals = BTreeMap; +pub type Withdrawals = NonEmptyMap; pub type RequiredSigners = NonEmptySet; @@ -392,8 +502,7 @@ pub struct DRepVotingThresholds { pub treasury_withdrawal: UnitInterval, } -#[derive(Serialize, Deserialize, Encode, Debug, PartialEq, Clone)] -#[cbor(map)] +#[derive(Encode, Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct TransactionBody<'a> { #[n(0)] pub inputs: Set, @@ -411,7 +520,7 @@ pub struct TransactionBody<'a> { pub certificates: Option>, #[n(5)] - pub withdrawals: Option>, + pub withdrawals: Option>, #[n(7)] pub auxiliary_data_hash: Option, @@ -420,7 +529,7 @@ pub struct TransactionBody<'a> { pub validity_interval_start: Option, #[n(9)] - pub mint: Option>, + pub mint: Option>, #[n(11)] pub script_data_hash: Option>, @@ -457,183 +566,420 @@ pub struct TransactionBody<'a> { pub donation: Option, } -// This is ugly but I'm not sure how to do it with minicbor-derive -// the cbor map implementation is here: -// https://github.com/twittner/minicbor/blob/83a4a0f868ac9ffc924a282f8f917aa2ad7c698a/minicbor-derive/src/decode.rs#L405-L424 -// We need to do validation inside the decoder or change the types of the validated fields to -// new types that do their own validation -impl<'b, C> minicbor::Decode<'b, C> for TransactionBody<'b> { - fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { - let mut must_inputs = None; - let mut must_outputs = None; - let mut must_fee = None; - let mut ttl = None; - let mut certificates = None; - let mut withdrawals = None; - let mut auxiliary_data_hash = None; - let mut validity_interval_start = None; - let mut mint: Option> = None; - let mut script_data_hash = None; - let mut collateral = None; - let mut required_signers = None; - let mut network_id = None; - let mut collateral_return = None; - let mut total_collateral = None; - let mut reference_inputs = None; - let mut voting_procedures = None; - let mut proposal_procedures = None; - let mut treasury_value = None; - let mut donation = None; - - let map_init = d.map()?; - let mut items_seen = 0; - - let mut seen_key = HashSet::new(); - - loop { - let n = d.i64(); - let Ok(index) = n else { break }; - if seen_key.contains(&index) { - return Err(minicbor::decode::Error::message("transaction body must not contain duplicate keys")); - } - match index { - 0 => { - must_inputs = d.decode_with(ctx)?; - }, - 1 => { - must_outputs = d.decode_with(ctx)?; - }, - 2 => { - must_fee = d.decode_with(ctx)?; - }, - 3 => { - ttl = d.decode_with(ctx)?; - }, - 4 => { - certificates = d.decode_with(ctx)?; - }, - 5 => { - let real_withdrawals: BTreeMap = d.decode_with(ctx)?; - if real_withdrawals.len() == 0 { - return Err(minicbor::decode::Error::message("withdrawals must be non-empty if present")); - } - withdrawals = Some(real_withdrawals); - }, - 7 => { - auxiliary_data_hash = d.decode_with(ctx)?; - }, - 8 => { - validity_interval_start = d.decode_with(ctx)?; - }, - 9 => { - let real_mint: Multiasset = d.decode_with(ctx)?; - if real_mint.0.len() == 0 { - return Err(minicbor::decode::Error::message("mint must be non-empty if present")); - } - mint = Some(real_mint); - }, - 11 => { - script_data_hash = d.decode_with(ctx)?; - }, - 13 => { - collateral = d.decode_with(ctx)?; - }, - 14 => { - required_signers = d.decode_with(ctx)?; - }, - 15 => { - network_id = d.decode_with(ctx)?; - }, - 16 => { - collateral_return = d.decode_with(ctx)?; - }, - 17 => { - total_collateral = d.decode_with(ctx)?; - }, - 18 => { - reference_inputs = d.decode_with(ctx)?; - }, - 19 => { - let real_voting_procedures: VotingProcedures = d.decode_with(ctx)?; - if real_voting_procedures.len() == 0 { - return Err(minicbor::decode::Error::message("voting procedures must be non-empty if present")); - } - voting_procedures = Some(real_voting_procedures); - }, - 20 => { - let real_proposal_procedures: NonEmptySet = d.decode_with(ctx)?; - if real_proposal_procedures.len() == 0 { - return Err(minicbor::decode::Error::message("proposal procedures must be non-empty if present")); +#[derive(Clone, Debug)] +enum TxBodyField<'a> { + Inputs(Set), + Outputs(Vec>), + Fee(Coin), + Ttl(Option), + Certificates(Option>), + Withdrawals(Option>), + AuxiliaryDataHash(Option), + ValidityIntervalStart(Option), + Mint(Option>), + ScriptDataHash(Option>), + Collateral(Option>), + RequiredSigners(Option), + NetworkId(Option), + CollateralReturn(Option>), + TotalCollateral(Option), + ReferenceInputs(Option>), + VotingProcedures(Option), + ProposalProcedures(Option>), + TreasuryValue(Option), + Donation(Option), +} + +fn decode_tx_body_field<'b, Ctx>(d: &mut minicbor::Decoder<'b>, k: u64, ctx: &mut Ctx) -> Result, minicbor::decode::Error> +where + Ctx: StrictContext +{ + match k { + 0 => { + let inputs = d.decode_with(ctx)?; + Ok(TxBodyField::Inputs(inputs)) + }, + 1 => { + let outputs = d.decode_with(ctx)?; + Ok(TxBodyField::Outputs(outputs)) + }, + 2 => { + let coin = d.decode_with(ctx)?; + Ok(TxBodyField::Fee(coin)) + }, + 3 => { + let ttl = d.decode_with(ctx)?; + Ok(TxBodyField::Ttl(ttl)) + }, + 4 => { + let certificates = d.decode_with(ctx)?; + Ok(TxBodyField::Certificates(certificates)) + }, + 5 => { + let withdrawals = d.decode_with(ctx)?; + Ok(TxBodyField::Withdrawals(withdrawals)) + } + 7 => { + let auxiliary_data_hash= d.decode_with(ctx)?; + Ok(TxBodyField::AuxiliaryDataHash(auxiliary_data_hash)) + } + 8 => { + let validity_interval_start = d.decode_with(ctx)?; + Ok(TxBodyField::ValidityIntervalStart(validity_interval_start)) + } + 9 => { + let mint = d.decode_with(ctx)?; + Ok(TxBodyField::Mint(mint)) + } + 11 => { + let script_data_hash = d.decode_with(ctx)?; + Ok(TxBodyField::ScriptDataHash(script_data_hash)) + } + 13 => { + let collateral = d.decode_with(ctx)?; + Ok(TxBodyField::Collateral(collateral)) + } + 14 => { + let required_signers = d.decode_with(ctx)?; + Ok(TxBodyField::RequiredSigners(required_signers)) + } + 15 => { + let network_id = d.decode_with(ctx)?; + Ok(TxBodyField::NetworkId(network_id)) + } + 16 => { + let collateral_return = d.decode_with(ctx)?; + Ok(TxBodyField::CollateralReturn(collateral_return)) + } + 17 => { + let total_collateral = d.decode_with(ctx)?; + Ok(TxBodyField::TotalCollateral(total_collateral)) + } + 18 => { + let reference_inputs = d.decode_with(ctx)?; + Ok(TxBodyField::ReferenceInputs(reference_inputs)) + } + 19 => { + let voting_procedures = d.decode_with(ctx)?; + Ok(TxBodyField::VotingProcedures(voting_procedures)) + } + 20 => { + let proposal_procedures = d.decode_with(ctx)?; + Ok(TxBodyField::ProposalProcedures(proposal_procedures)) + } + 21 => { + let treasury_value = d.decode_with(ctx)?; + Ok(TxBodyField::TreasuryValue(treasury_value)) + } + 22 => { + let donation = d.decode_with(ctx)?; + Ok(TxBodyField::Donation(donation)) + } + k => Err(minicbor::decode::Error::message(format!("Unknown txbody field key {}", k))) + } +} + +struct TxBodyFields<'b> { + entries: BTreeMap>>, +} + +impl <'b, Ctx> minicbor::Decode<'b, Ctx> for TxBodyFields<'b> +where + Ctx: StrictContext +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { + let mut entries = BTreeMap::new(); + let map_size = d.map()?; + match map_size { + None => { + loop { + let ty = d.datatype()?; + if ty == Type::Break { + d.skip()?; + break; } - proposal_procedures = Some(real_proposal_procedures); - }, - 21 => { - treasury_value = d.decode_with(ctx)?; - }, - 22 => { - donation = d.decode_with(ctx)?; - }, - _ => { - return Err(minicbor::decode::Error::message("unexpected index")); + let k = d.u64()?; + let v = decode_tx_body_field(d, k, ctx)?; + entries.entry(k).and_modify(|ar: &mut Vec>| ar.push(v.clone())).or_insert(vec![v]); } - } - seen_key.insert(index); - items_seen += 1; - if let Some(map_count) = map_init { - if items_seen == map_count { - break; + }, + Some(n) => { + for _ in 0..n { + let k = d.u64()?; + let v = decode_tx_body_field(d, k, ctx)?; + entries.entry(k).and_modify(|ar: &mut Vec>| ar.push(v.clone())).or_insert(vec![v]); } } } + Ok(TxBodyFields { + entries + }) + } +} - if map_init.is_some_and(|map_count| map_count != items_seen) { - return Err(minicbor::decode::Error::message("map is not valid cbor: declared count did not match actual count")); +fn make_basic_tx_body<'a>(inputs: Set, outputs: Vec>, fee: Coin) -> TransactionBody<'a> { + TransactionBody { + inputs, + outputs, + fee, + ttl: None, + certificates: None, + withdrawals: None, + auxiliary_data_hash: None, + validity_interval_start: None, + mint: None, + script_data_hash: None, + collateral: None, + required_signers: None, + network_id: None, + collateral_return: None, + total_collateral: None, + reference_inputs: None, + voting_procedures: None, + proposal_procedures: None, + treasury_value: None, + donation: None, + } +} + +fn set_tx_body_field<'a>(txbody: &mut TransactionBody<'a>, index: u64, field: TxBodyField<'a>) -> Result<(), String> { + match (index, field) { + (0, TxBodyField::Inputs(i)) => { + txbody.inputs = i; + }, + (1, TxBodyField::Outputs(o)) => { + txbody.outputs = o; + }, + (2, TxBodyField::Fee(f)) => { + txbody.fee = f; + } + (3, TxBodyField::Ttl(t)) => { + txbody.ttl = t; + } + (4, TxBodyField::Certificates(c)) => { + txbody.certificates = c; + } + (5, TxBodyField::Withdrawals(w)) => { + txbody.withdrawals = w; + } + (7, TxBodyField::AuxiliaryDataHash(a)) => { + txbody.auxiliary_data_hash = a; + } + (8, TxBodyField::ValidityIntervalStart(v)) => { + txbody.validity_interval_start = v; + } + (9, TxBodyField::Mint(m)) => { + txbody.mint = m; + } + (11, TxBodyField::ScriptDataHash(s)) => { + txbody.script_data_hash = s; + } + (13, TxBodyField::Collateral(c)) => { + txbody.collateral = c; + } + (14, TxBodyField::RequiredSigners(r)) => { + txbody.required_signers = r; } + (15, TxBodyField::NetworkId(n)) => { + txbody.network_id = n; + } + (16, TxBodyField::CollateralReturn(c)) => { + txbody.collateral_return = c; + } + (17, TxBodyField::TotalCollateral(t)) => { + txbody.total_collateral = t; + } + (18, TxBodyField::ReferenceInputs(r)) => { + txbody.reference_inputs = r; + } + (19, TxBodyField::VotingProcedures(v)) => { + txbody.voting_procedures = v; + } + (20, TxBodyField::ProposalProcedures(p)) => { + txbody.proposal_procedures = p; + } + (21, TxBodyField::TreasuryValue(t)) => { + txbody.treasury_value = t; + } + (22, TxBodyField::Donation(d)) => { + txbody.donation = d; + } + (ix, f) => { + return Err(format!("Wrong index {} for txbody field {:?}", ix, f)) + } + } + Ok(()) +} - if map_init.is_none() { - let ty = d.datatype()?; - if ty == minicbor::data::Type::Break { - d.skip()?; - } else { - return Err(minicbor::decode::Error::message("unexpected garbage at end of map")); +impl <'b, Ctx> minicbor::Decode<'b, Ctx> for TransactionBody<'b> +where + Ctx: StrictContext +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { + let fields: TxBodyFields<'b> = d.decode_with(ctx)?; + let entries = fields.entries; + let inputs = entries.get(&0).and_then(|v| v.first()); + let outputs = entries.get(&1).and_then(|v| v.first()); + let fee = entries.get(&2).and_then(|v| v.first()); + let mut tx_body = match (inputs, outputs, fee) { + (Some(TxBodyField::Inputs(inputs)), Some(TxBodyField::Outputs(outputs)), Some(TxBodyField::Fee(fee))) => { + make_basic_tx_body(inputs.clone(), outputs.clone(), *fee) + }, + _ => { + return Err(minicbor::decode::Error::message("inputs, outputs, and fee fields are required")) + }, + }; + for (key, val) in entries { + if val.len() > 1 { + ctx.push_error(format!("duplicate txbody entries for key {}", key)) + } + match val.first() { + Some(first) => { + let result = set_tx_body_field(&mut tx_body, key, first.clone()); + if let Err(e) = result { + return Err(minicbor::decode::Error::message( + format!("could not set txbody field: {}", e) + )); + } + }, + None => { + // This is impossible because we always initialize TxBodyFields entries with + // singleton arrays. Could maybe use a NonEmpty Vec type to eliminate this + // branch + return Err(minicbor::decode::Error::message("TxBodyFields entry was empty")) + } } } + Ok(tx_body) + } +} - let Some(inputs) = must_inputs else { - return Err(minicbor::decode::Error::message("field inputs is required")); - }; - let Some(outputs) = must_outputs else { - return Err(minicbor::decode::Error::message("field outputs is required")); - }; - let Some(fee) = must_fee else { - return Err(minicbor::decode::Error::message("field fee is required")); - }; +#[derive(Debug, PartialEq, Clone)] +pub struct StrictTransactionBody<'b> { + inner: TransactionBody<'b> +} - Ok(Self { - inputs, - outputs, - fee, - ttl, - certificates, - withdrawals, - auxiliary_data_hash, - validity_interval_start, - mint, - script_data_hash, - collateral, - required_signers, - network_id, - collateral_return, - total_collateral, - reference_inputs, - voting_procedures, - proposal_procedures, - treasury_value, - donation, +impl <'b, Ctx> minicbor::Decode<'b, Ctx> for StrictTransactionBody<'b> +where + Ctx: StrictContext +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { + let inner = d.decode_with(ctx)?; + let errs = ctx.get_errors(); + if !errs.is_empty() { + let s = errs.join(";"); + return Err(minicbor::decode::Error::message( + format!("TransactionBody failed strict validations: {}", s) + )); + } + Ok(StrictTransactionBody { + inner }) } } + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct NonEmptyMap { + #[serde(bound(deserialize = "K: Deserialize<'de> + Ord, V: Deserialize<'de>"))] + map: BTreeMap +} + +impl minicbor::Encode for NonEmptyMap +where + K: minicbor::Encode + Ord, + V: minicbor::Encode +{ + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.encode_with(&self.map, ctx)?; + Ok(()) + } +} + +impl <'b, Ctx, K, V> minicbor::Decode<'b, Ctx> for NonEmptyMap +where + K: minicbor::Decode<'b, Ctx> + Eq + Ord, + V: minicbor::Decode<'b, Ctx>, + Ctx: StrictContext +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { + let map: BTreeMap = d.decode_with(ctx)?; + if map.is_empty() { + ctx.push_error("map must not be empty".to_string()); + } + Ok(NonEmptyMap { map }) + } +} + +impl std::ops::Deref for NonEmptyMap { + type Target = BTreeMap; + + fn deref(&self) -> &Self::Target { + &self.map + } +} + + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct NonEmptyMultiasset { + asset: Multiasset +} + +impl minicbor::Encode for NonEmptyMultiasset +where T: minicbor::Encode +{ + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.encode_with(&self.asset, ctx)?; + Ok(()) + } +} + +impl <'b, Ctx, T> minicbor::Decode<'b, Ctx> for NonEmptyMultiasset +where + T: minicbor::Decode<'b, Ctx>, + Ctx: StrictContext +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { + let asset: Multiasset = d.decode_with(ctx)?; + if asset.0.is_empty() { + ctx.push_error("multiasset must not be empty".to_string()); + } + Ok(NonEmptyMultiasset { asset }) + } +} + +impl std::ops::Deref for NonEmptyMultiasset { + type Target = BTreeMap>; + + fn deref(&self) -> &Self::Target { + &self.asset.0 + } +} + +impl NonEmptyMultiasset { + pub fn from_multiasset(ma: Multiasset) -> Option { + if ma.is_empty() { + None + } else { + Some(NonEmptyMultiasset { + asset: ma, + }) + } + } + + pub fn to_multiasset(self) -> Multiasset { + self.asset + } +} + #[deprecated(since = "1.0.0-alpha", note = "use `TransactionBody` instead")] pub type MintedTransactionBody<'a> = TransactionBody<'a>; @@ -648,7 +994,7 @@ pub enum Vote { Abstain, } -pub type VotingProcedures = BTreeMap>; +pub type VotingProcedures = NonEmptyMap>; #[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct VotingProcedure { @@ -751,12 +1097,44 @@ pub type MintedPostAlonzoTransactionOutput<'b> = PostAlonzoTransactionOutput<'b> pub type TransactionOutput<'b> = babbage::GenTransactionOutput<'b, PostAlonzoTransactionOutput<'b>>; -// FIXME: Repeated since macro does not handle type generics yet. -codec_by_datatype! { - TransactionOutput<'b>, - Array | ArrayIndef => Legacy, - Map | MapIndef => PostAlonzo, - () +impl<'b, C> minicbor::Encode for TransactionOutput<'b> { + fn encode( + &self, + e: &mut minicbor::Encoder, + _ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + match self { + TransactionOutput::Legacy(legacy) => { + e.encode(legacy)?; + Ok(()) + } + TransactionOutput::PostAlonzo(post_alonzo) => { + e.encode(post_alonzo)?; + Ok(()) + } + } + } +} + +impl<'b, Ctx> minicbor::Decode<'b, Ctx> for TransactionOutput<'b> +where + Ctx: StrictContext +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { + match d.datatype()? { + Type::Array | Type::ArrayIndef => { + let legacy = d.decode_with(ctx)?; + Ok(TransactionOutput::Legacy(legacy)) + } + Type::Map | Type::MapIndef => { + let post_alonzo = d.decode_with(ctx)?; + Ok(TransactionOutput::PostAlonzo(post_alonzo)) + } + _ => { + Err(minicbor::decode::Error::message("Expected array or map")) + } + } + } } #[deprecated(since = "1.0.0-alpha", note = "use `TransactionOutput` instead")] @@ -874,6 +1252,30 @@ pub struct WitnessSet<'b> { pub plutus_v3_script: Option>>, } +#[derive(Debug, PartialEq, Clone)] +pub struct StrictWitnessSet<'b> { + inner: WitnessSet<'b> +} + +impl<'b, Ctx> minicbor::Decode<'b, Ctx> for StrictWitnessSet<'b> +where + Ctx: StrictContext +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { + let inner: WitnessSet<'b> = d.decode_with(ctx)?; + let errs = ctx.get_errors(); + if !errs.is_empty() { + let s = errs.join(";"); + return Err(minicbor::decode::Error::message( + format!("WitnessSet failed strict validations: {}", s) + )); + } + Ok(StrictWitnessSet { + inner + }) + } +} + #[deprecated(since = "1.0.0-alpha", note = "use `WitnessSet` instead")] pub type MintedWitnessSet<'b> = WitnessSet<'b>; @@ -953,6 +1355,7 @@ pub use crate::alonzo::AuxiliaryData; /// original CBOR bytes for each structure that might require hashing. In this /// way, we make sure that the resulting hash matches what exists on-chain. #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)] +#[cbor(context_bound = "StrictContext")] pub struct Block<'b> { #[n(0)] pub header: KeepRaw<'b, Header>, @@ -974,6 +1377,7 @@ pub struct Block<'b> { pub type MintedBlock<'b> = Block<'b>; #[derive(Clone, Serialize, Deserialize, Encode, Decode, Debug)] +#[cbor(context_bound = "StrictContext")] pub struct Tx<'b> { #[b(0)] pub transaction_body: KeepRaw<'b, TransactionBody<'b>>, @@ -997,7 +1401,7 @@ fn is_multiasset_small_enough(ma: &Multiasset) -> bool { let policy_count = ma.0.len(); let mut asset_count = 0; - for (_policy, assets) in &ma.0 { + for assets in ma.0.values() { asset_count += assets.len(); } @@ -1014,42 +1418,43 @@ mod tests { #[cfg(test)] mod tests_value { - use super::super::Mint; - use super::super::Multiasset; + use super::super::BasicStrictContext; + use super::super::{StrictMint}; + use super::super::{Multiasset, StrictMultiasset}; use super::super::NonZeroInt; - use super::super::Value; + use super::super::{Value, StrictValue}; use pallas_codec::minicbor; use std::collections::BTreeMap; // a value can have zero coins and omit the multiasset #[test] fn decode_zero_value() { - let ma: Value = minicbor::decode(&hex::decode("00").unwrap()).unwrap(); - assert_eq!(ma, Value::Coin(0)); + let ma: StrictValue = minicbor::decode_with(&hex::decode("00").unwrap(), &mut BasicStrictContext::new()).unwrap(); + assert_eq!(ma.inner, Value::Coin(0)); } // a value can have zero coins and an empty multiasset map // Note: this will roundtrip back to "00" #[test] fn permit_definite_value() { - let ma: Value = minicbor::decode(&hex::decode("8200a0").unwrap()).unwrap(); - assert_eq!(ma, Value::Multiasset(0, Multiasset(BTreeMap::new()))); + let ma: StrictValue = minicbor::decode_with(&hex::decode("8200a0").unwrap(), &mut BasicStrictContext::new()).unwrap(); + assert_eq!(ma.inner, Value::Multiasset(0, Multiasset(BTreeMap::new()))); } // Indefinite-encoded value is valid #[test] fn permit_indefinite_value() { - let ma: Value = minicbor::decode(&hex::decode("9f00a0ff").unwrap()).unwrap(); - assert_eq!(ma, Value::Multiasset(0, Multiasset(BTreeMap::new()))); + let ma: StrictValue = minicbor::decode_with(&hex::decode("9f00a0ff").unwrap(), &mut BasicStrictContext::new()).unwrap(); + assert_eq!(ma.inner, Value::Multiasset(0, Multiasset(BTreeMap::new()))); } // the asset sub-map of a policy map in a multiasset must not be null in Conway #[test] fn reject_null_tokens() { - let ma: Result = minicbor::decode(&hex::decode("8200a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap()); + let ma: Result = minicbor::decode_with(&hex::decode("8200a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap(), &mut BasicStrictContext::new()); assert_eq!( ma.map_err(|e| e.to_string()), - Err("decode error: Policy must not be empty".to_owned()) + Err("decode error: Value failed strict validations: Policy must not be empty".to_owned()) ); } @@ -1057,7 +1462,7 @@ mod tests { // Conway #[test] fn reject_zero_tokens() { - let ma: Result = minicbor::decode(&hex::decode("8200a1581c00000000000000000000000000000000000000000000000000000000a14000").unwrap()); + let ma: Result = minicbor::decode_with(&hex::decode("8200a1581c00000000000000000000000000000000000000000000000000000000a14000").unwrap(), &mut BasicStrictContext::new()); assert_eq!( ma.map_err(|e| e.to_string()), Err("decode error: PositiveCoin must not be 0".to_owned()) @@ -1066,10 +1471,10 @@ mod tests { #[test] fn multiasset_reject_null_tokens() { - let ma: Result, _> = minicbor::decode(&hex::decode("a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap()); + let ma: Result, _> = minicbor::decode_with(&hex::decode("a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap(), &mut BasicStrictContext::new()); assert_eq!( ma.map_err(|e| e.to_string()), - Err("decode error: Policy must not be empty".to_owned()) + Err("decode error: Multiasset failed strict validations: Policy must not be empty".to_owned()) ); } @@ -1087,38 +1492,38 @@ mod tests { // minimal token map (conway requires nonempty asset maps) s += "a14001"; } - let ma: Result, _> = minicbor::decode(&hex::decode(s).unwrap()); + let ma: Result, _> = minicbor::decode_with(&hex::decode(s).unwrap(), &mut BasicStrictContext::new()); match ma { Ok(_) => panic!("decode succeded but should fail"), - Err(e) => assert_eq!(e.to_string(), "decode error: Multiasset must not exceed size limit") + Err(e) => assert_eq!(e.to_string(), "decode error: Multiasset failed strict validations: Multiasset must not exceed size limit") } } #[test] fn mint_reject_null_tokens() { - let ma: Result = minicbor::decode(&hex::decode("a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap()); + let ma: Result = minicbor::decode_with(&hex::decode("a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap(), &mut BasicStrictContext::new()); assert_eq!( ma.map_err(|e| e.to_string()), - Err("decode error: Policy must not be empty".to_owned()) + Err("decode error: Mint failed strict validations: Policy must not be empty".to_owned()) ); } } mod tests_witness_set { - use super::super::{Bytes, VKeyWitness, WitnessSet}; + use super::super::{BasicStrictContext, Bytes, VKeyWitness, WitnessSet, StrictWitnessSet}; use pallas_codec::minicbor; #[test] fn decode_empty_witness_set() { let witness_set_bytes = hex::decode("a0").unwrap(); - let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); + let ws: WitnessSet = minicbor::decode_with(&witness_set_bytes, &mut BasicStrictContext::new()).unwrap(); assert_eq!(ws.vkeywitness, None); } #[test] fn decode_witness_set_having_vkeywitness_untagged_must_be_nonempty() { let witness_set_bytes = hex::decode("a10080").unwrap(); - let ws: Result = minicbor::decode(&witness_set_bytes); + let ws: Result = minicbor::decode_with(&witness_set_bytes, &mut BasicStrictContext::new()); assert_eq!( ws.map_err(|e| e.to_string()), Err("decode error: decoding empty set as NonEmptySet".to_owned()) @@ -1128,31 +1533,31 @@ mod tests { #[test] fn decode_witness_set_having_vkeywitness_untagged_singleton() { let witness_set_bytes = hex::decode("a10081824040").unwrap(); - let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); + let ws: StrictWitnessSet = minicbor::decode_with(&witness_set_bytes, &mut BasicStrictContext::new()).unwrap(); let expected = VKeyWitness { vkey: Bytes::from(vec![]), signature: Bytes::from(vec![]), }; - assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![expected])); + assert_eq!(ws.inner.vkeywitness.map(|s| s.to_vec()), Some(vec![expected])); } #[test] fn decode_witness_set_having_vkeywitness_conwaystyle_singleton() { let witness_set_bytes = hex::decode("a100d9010281824040").unwrap(); - let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); + let ws: StrictWitnessSet = minicbor::decode_with(&witness_set_bytes, &mut BasicStrictContext::new()).unwrap(); let expected = VKeyWitness { vkey: Bytes::from(vec![]), signature: Bytes::from(vec![]), }; - assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![expected])); + assert_eq!(ws.inner.vkeywitness.map(|s| s.to_vec()), Some(vec![expected])); } #[test] fn decode_witness_set_having_vkeywitness_conwaystyle_must_be_nonempty() { let witness_set_bytes = hex::decode("a100d9010280").unwrap(); - let ws: Result = minicbor::decode(&witness_set_bytes); + let ws: Result = minicbor::decode_with(&witness_set_bytes, &mut BasicStrictContext::new()); assert_eq!( ws.map_err(|e| e.to_string()), Err("decode error: decoding empty set as NonEmptySet".to_owned()) @@ -1163,7 +1568,7 @@ mod tests { fn decode_witness_set_having_vkeywitness_reject_nonsense_tag() { // VKey witness set with nonsense tag 259 let witness_set_bytes = hex::decode("a100d9010381824040").unwrap(); - let ws: Result = minicbor::decode(&witness_set_bytes); + let ws: Result = minicbor::decode_with(&witness_set_bytes, &mut BasicStrictContext::new()); assert_eq!( ws.map_err(|e| e.to_string()), Err("decode error: Unrecognised tag: Tag(259)".to_owned()) @@ -1178,13 +1583,13 @@ mod tests { #[test] fn decode_witness_set_having_vkeywitness_duplicate_entries() { let witness_set_bytes = hex::decode("a100d9010282824040824040").unwrap(); - let ws: WitnessSet = minicbor::decode(&witness_set_bytes).unwrap(); + let ws: StrictWitnessSet = minicbor::decode_with(&witness_set_bytes, &mut BasicStrictContext::new()).unwrap(); let expected = VKeyWitness { vkey: Bytes::from(vec![]), signature: Bytes::from(vec![]), }; - assert_eq!(ws.vkeywitness.map(|s| s.to_vec()), Some(vec![expected.clone(), expected])); + assert_eq!(ws.inner.vkeywitness.map(|s| s.to_vec()), Some(vec![expected.clone(), expected])); } } @@ -1241,7 +1646,7 @@ mod tests { } mod tests_transaction { - use super::super::TransactionBody; + use super::super::{BasicStrictContext, TransactionBody, StrictTransactionBody}; use pallas_codec::minicbor; // A simple tx with just inputs, outputs, and fee. Address is not well-formed, since the @@ -1249,7 +1654,8 @@ mod tests { #[test] fn decode_simple_tx() { let tx_bytes = hex::decode("a300828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a04000000").unwrap(); - let tx: TransactionBody = minicbor::decode(&tx_bytes).unwrap(); + let tx: StrictTransactionBody = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()).unwrap(); + let tx: TransactionBody = tx.inner; assert_eq!(tx.fee, 0); } @@ -1258,10 +1664,10 @@ mod tests { #[test] fn reject_empty_tx() { let tx_bytes = hex::decode("a0").unwrap(); - let tx: Result, _> = minicbor::decode(&tx_bytes); + let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), - Err("decode error: field inputs is required".to_owned()) + Err("decode error: inputs, outputs, and fee fields are required".to_owned()) ); } @@ -1269,10 +1675,10 @@ mod tests { #[test] fn reject_tx_missing_outputs() { let tx_bytes = hex::decode("a200818258200000000000000000000000000000000000000000000000000000000000000008090200").unwrap(); - let tx: Result, _> = minicbor::decode(&tx_bytes); + let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), - Err("decode error: field outputs is required".to_owned()) + Err("decode error: inputs, outputs, and fee fields are required".to_owned()) ); } @@ -1280,10 +1686,10 @@ mod tests { #[test] fn reject_tx_missing_fee() { let tx_bytes = hex::decode("a20081825820000000000000000000000000000000000000000000000000000000000000000809018182581c000000000000000000000000000000000000000000000000000000001affffffff").unwrap(); - let tx: Result, _> = minicbor::decode(&tx_bytes); + let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), - Err("decode error: field fee is required".to_owned()) + Err("decode error: inputs, outputs, and fee fields are required".to_owned()) ); } @@ -1293,17 +1699,17 @@ mod tests { #[test] fn reject_empty_present_mint() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a0400000009a0").unwrap(); - let tx: Result, _> = minicbor::decode(&tx_bytes); + let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), - Err("decode error: mint must be non-empty if present".to_owned()) + Err("decode error: TransactionBody failed strict validations: multiasset must not be empty".to_owned()) ); } #[test] fn reject_empty_present_certs() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000000480").unwrap(); - let tx: Result, _> = minicbor::decode(&tx_bytes); + let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), Err("decode error: decoding empty set as NonEmptySet".to_owned()) @@ -1313,17 +1719,17 @@ mod tests { #[test] fn reject_empty_present_withdrawals() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a0400000005a0").unwrap(); - let tx: Result, _> = minicbor::decode(&tx_bytes); + let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), - Err("decode error: withdrawals must be non-empty if present".to_owned()) + Err("decode error: TransactionBody failed strict validations: map must not be empty".to_owned()) ); } #[test] fn reject_empty_present_collateral_inputs() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000000d80").unwrap(); - let tx: Result, _> = minicbor::decode(&tx_bytes); + let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), Err("decode error: decoding empty set as NonEmptySet".to_owned()) @@ -1333,7 +1739,7 @@ mod tests { #[test] fn reject_empty_present_required_signers() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000000e80").unwrap(); - let tx: Result, _> = minicbor::decode(&tx_bytes); + let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), Err("decode error: decoding empty set as NonEmptySet".to_owned()) @@ -1343,17 +1749,17 @@ mod tests { #[test] fn reject_empty_present_voting_procedures() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a0400000013a0").unwrap(); - let tx: Result, _> = minicbor::decode(&tx_bytes); + let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), - Err("decode error: voting procedures must be non-empty if present".to_owned()) + Err("decode error: TransactionBody failed strict validations: map must not be empty".to_owned()) ); } #[test] fn reject_empty_present_proposal_procedures() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000001480").unwrap(); - let tx: Result, _> = minicbor::decode(&tx_bytes); + let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), Err("decode error: decoding empty set as NonEmptySet".to_owned()) @@ -1363,7 +1769,7 @@ mod tests { #[test] fn reject_empty_present_donation() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000001600").unwrap(); - let tx: Result, _> = minicbor::decode(&tx_bytes); + let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), Err("decode error: PositiveCoin must not be 0".to_owned()) @@ -1374,10 +1780,10 @@ mod tests { #[test] fn reject_duplicate_keys() { let tx_bytes = hex::decode("a40081825820000000000000000000000000000000000000000000000000000000000000000809018182581c000000000000000000000000000000000000000000000000000000001affffffff02010201").unwrap(); - let tx: Result, _> = minicbor::decode(&tx_bytes); + let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), - Err("decode error: transaction body must not contain duplicate keys".to_owned()) + Err("decode error: TransactionBody failed strict validations: duplicate txbody entries for key 2".to_owned()) ); } } diff --git a/pallas-txbuilder/src/conway.rs b/pallas-txbuilder/src/conway.rs index f99b9b009..55cadc2f0 100644 --- a/pallas-txbuilder/src/conway.rs +++ b/pallas-txbuilder/src/conway.rs @@ -7,7 +7,7 @@ use pallas_primitives::{ DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, NonZeroInt, PlutusData, PlutusScript, PostAlonzoTransactionOutput, Redeemer, RedeemerTag, ScriptRef as PallasScript, TransactionBody, TransactionInput, TransactionOutput, Tx, Value, - WitnessSet, + WitnessSet, NonEmptyMultiasset, }, Fragment, NonEmptySet, PositiveCoin, }; @@ -68,11 +68,7 @@ impl BuildConway for StagingTransaction { ) }) .collect::>(); - let mint = if mint.is_empty() { - None - } else { - Some(mint.into_iter().collect()) - }; + let mint = NonEmptyMultiasset::from_multiasset(mint.into_iter().collect()); let collateral = NonEmptySet::from_vec( self.collateral_inputs @@ -160,7 +156,7 @@ impl BuildConway for StagingTransaction { let mut mint_policies = mint .iter() - .flat_map(|x: &pallas_primitives::conway::Multiasset| x.iter()) + .flat_map(|x: &pallas_primitives::conway::NonEmptyMultiasset| x.iter()) .map(|(p, _)| *p) .collect::>(); diff --git a/pallas-validate/src/phase1/conway.rs b/pallas-validate/src/phase1/conway.rs index f58678eba..802a0ab6d 100644 --- a/pallas-validate/src/phase1/conway.rs +++ b/pallas-validate/src/phase1/conway.rs @@ -365,7 +365,7 @@ fn check_preservation_of_value(tx_body: &TransactionBody, utxos: &UTxOs) -> Vali &PostAlonzo(NegativeValue), )?; if let Some(m) = &tx_body.mint { - input = conway_add_minted_non_zero(&input, m, &PostAlonzo(NegativeValue))?; + input = conway_add_minted_non_zero(&input, &m.clone().to_multiasset(), &PostAlonzo(NegativeValue))?; } if !conway_values_are_equal(&input, &output) { From 11de00920b80226f0e99e104e14cd2b959837199 Mon Sep 17 00:00:00 2001 From: ruko Date: Wed, 29 Oct 2025 13:36:13 -0700 Subject: [PATCH 11/11] implement generic Strict/StrictVerbose wrappers --- pallas-primitives/src/conway/model.rs | 332 +++++++++++--------------- 1 file changed, 144 insertions(+), 188 deletions(-) diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index 92b4d724e..f33d78d8f 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -29,40 +29,116 @@ pub use crate::babbage::Header; use pallas_codec::minicbor::data::Type; -trait StrictContext { - fn push_error(&mut self, s: String); +trait ValidationContext { + fn push_error(&mut self, s: String) -> Result<(), minicbor::decode::Error>; fn get_errors(&self) -> &[String]; } -pub struct BasicStrictContext { +impl ValidationContext for () { + fn push_error(&mut self, _s: String) -> Result<(), minicbor::decode::Error> { + Ok(()) + } + fn get_errors(&self) -> &[String] { + &[] + } +} + +pub struct AccumulatingContext { errors: Vec, } -impl BasicStrictContext { +impl AccumulatingContext { pub fn new() -> Self { - BasicStrictContext { + AccumulatingContext { errors: vec![] } } } -impl StrictContext for BasicStrictContext { - fn push_error(&mut self, s: String) { - self.errors.push(s) +impl ValidationContext for AccumulatingContext { + fn push_error(&mut self, s: String) -> Result<(), minicbor::decode::Error> { + self.errors.push(s); + Ok(()) } fn get_errors(&self) -> &[String] { &self.errors } } -impl StrictContext for () { - fn push_error(&mut self, _s: String) { +pub struct TerminatingContext { +} + +impl TerminatingContext { + pub fn new() -> Self { + TerminatingContext { + } + } +} + +impl ValidationContext for TerminatingContext { + fn push_error(&mut self, s: String) -> Result<(), minicbor::decode::Error> { + Err(minicbor::decode::Error::message(format!("Failed strict validation: {}", s))) } fn get_errors(&self) -> &[String] { &[] } } +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Strict { + inner: T, +} + +impl Strict { + pub fn unwrap(self) -> T { + self.inner + } +} + +impl<'b, T, C> minicbor::Decode<'b, C> for Strict +where + T: minicbor::Decode<'b, TerminatingContext> +{ + fn decode(d: &mut minicbor::Decoder<'b>, _ctx: &mut C) -> Result { + let mut ctx = TerminatingContext::new(); + let inner: T = d.decode_with(&mut ctx)?; + Ok(Strict { + inner + }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct StrictVerbose { + inner: T, +} + +impl StrictVerbose { + pub fn unwrap(self) -> T { + self.inner + } +} + +impl<'b, T, C> minicbor::Decode<'b, C> for StrictVerbose +where + T: minicbor::Decode<'b, AccumulatingContext> +{ + fn decode(d: &mut minicbor::Decoder<'b>, _ctx: &mut C) -> Result { + let mut ctx = AccumulatingContext::new(); + let inner: T = d.decode_with(&mut ctx)?; + let errs = ctx.get_errors(); + if !errs.is_empty() { + let s = errs.join(";"); + return Err(minicbor::decode::Error::message( + format!("Failed strict validation: {}", s) + )); + } + Ok(StrictVerbose { + inner + }) + } +} + #[derive(Serialize, Deserialize, Encode, Debug, PartialEq, Eq, Clone)] pub struct Multiasset(#[n(0)] BTreeMap>); @@ -89,7 +165,7 @@ impl std::ops::Deref for Multiasset { impl<'b, Ctx, A: minicbor::Decode<'b, Ctx>> minicbor::Decode<'b, Ctx> for Multiasset where - Ctx: StrictContext + Ctx: ValidationContext { fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { let policies: BTreeMap> = d.decode_with(ctx)?; @@ -99,68 +175,20 @@ where // monomorphic? for assets in policies.values() { if assets.is_empty() { - ctx.push_error("Policy must not be empty".to_string()); + ctx.push_error("Policy must not be empty".to_string())?; } } let result = Multiasset(policies); if !is_multiasset_small_enough(&result) { - ctx.push_error("Multiasset must not exceed size limit".to_string()); + ctx.push_error("Multiasset must not exceed size limit".to_string())?; } Ok(result) } } -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct StrictMultiasset { - inner: Multiasset -} - -impl<'b, Ctx, A: minicbor::Decode<'b, Ctx>> minicbor::Decode<'b, Ctx> for StrictMultiasset -where - Ctx: StrictContext -{ - fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { - let inner: Multiasset = d.decode_with(ctx)?; - let errs = ctx.get_errors(); - if !errs.is_empty() { - let s = errs.join(";"); - return Err(minicbor::decode::Error::message( - format!("Multiasset failed strict validations: {}", s) - )); - } - Ok(StrictMultiasset { - inner - }) - } -} - pub type Mint = NonEmptyMultiasset; -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct StrictMint { - inner: Mint -} - -impl<'b, Ctx> minicbor::Decode<'b, Ctx> for StrictMint -where - Ctx: StrictContext -{ - fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { - let inner: Mint = d.decode_with(ctx)?; - let errs = ctx.get_errors(); - if !errs.is_empty() { - let s = errs.join(";"); - return Err(minicbor::decode::Error::message( - format!("Mint failed strict validations: {}", s) - )); - } - Ok(StrictMint { - inner - }) - } -} - #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub enum Value { Coin(Coin), @@ -189,7 +217,7 @@ impl minicbor::Encode for Value { impl<'b, Ctx> minicbor::Decode<'b, Ctx> for Value where - Ctx: StrictContext + Ctx: ValidationContext { fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { match d.datatype()? { @@ -210,30 +238,6 @@ where } } -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct StrictValue { - inner: Value -} - -impl<'b, Ctx> minicbor::Decode<'b, Ctx> for StrictValue -where - Ctx: StrictContext -{ - fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { - let inner: Value = d.decode_with(ctx)?; - let errs = ctx.get_errors(); - if !errs.is_empty() { - let s = errs.join(";"); - return Err(minicbor::decode::Error::message( - format!("Value failed strict validations: {}", s) - )); - } - Ok(StrictValue { - inner - }) - } -} - pub use crate::alonzo::TransactionOutput as LegacyTransactionOutput; pub type Withdrawals = NonEmptyMap; @@ -592,7 +596,7 @@ enum TxBodyField<'a> { fn decode_tx_body_field<'b, Ctx>(d: &mut minicbor::Decoder<'b>, k: u64, ctx: &mut Ctx) -> Result, minicbor::decode::Error> where - Ctx: StrictContext + Ctx: ValidationContext { match k { 0 => { @@ -685,7 +689,7 @@ struct TxBodyFields<'b> { impl <'b, Ctx> minicbor::Decode<'b, Ctx> for TxBodyFields<'b> where - Ctx: StrictContext + Ctx: ValidationContext { fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { let mut entries = BTreeMap::new(); @@ -813,7 +817,7 @@ fn set_tx_body_field<'a>(txbody: &mut TransactionBody<'a>, index: u64, field: Tx impl <'b, Ctx> minicbor::Decode<'b, Ctx> for TransactionBody<'b> where - Ctx: StrictContext + Ctx: ValidationContext { fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { let fields: TxBodyFields<'b> = d.decode_with(ctx)?; @@ -831,7 +835,7 @@ where }; for (key, val) in entries { if val.len() > 1 { - ctx.push_error(format!("duplicate txbody entries for key {}", key)) + ctx.push_error(format!("duplicate txbody entries for key {}", key))?; } match val.first() { Some(first) => { @@ -854,31 +858,6 @@ where } } -#[derive(Debug, PartialEq, Clone)] -pub struct StrictTransactionBody<'b> { - inner: TransactionBody<'b> -} - -impl <'b, Ctx> minicbor::Decode<'b, Ctx> for StrictTransactionBody<'b> -where - Ctx: StrictContext -{ - fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { - let inner = d.decode_with(ctx)?; - let errs = ctx.get_errors(); - if !errs.is_empty() { - let s = errs.join(";"); - return Err(minicbor::decode::Error::message( - format!("TransactionBody failed strict validations: {}", s) - )); - } - Ok(StrictTransactionBody { - inner - }) - } -} - - #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct NonEmptyMap { #[serde(bound(deserialize = "K: Deserialize<'de> + Ord, V: Deserialize<'de>"))] @@ -904,12 +883,12 @@ impl <'b, Ctx, K, V> minicbor::Decode<'b, Ctx> for NonEmptyMap where K: minicbor::Decode<'b, Ctx> + Eq + Ord, V: minicbor::Decode<'b, Ctx>, - Ctx: StrictContext + Ctx: ValidationContext { fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { let map: BTreeMap = d.decode_with(ctx)?; if map.is_empty() { - ctx.push_error("map must not be empty".to_string()); + ctx.push_error("map must not be empty".to_string())?; } Ok(NonEmptyMap { map }) } @@ -945,12 +924,12 @@ where T: minicbor::Encode impl <'b, Ctx, T> minicbor::Decode<'b, Ctx> for NonEmptyMultiasset where T: minicbor::Decode<'b, Ctx>, - Ctx: StrictContext + Ctx: ValidationContext { fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { let asset: Multiasset = d.decode_with(ctx)?; if asset.0.is_empty() { - ctx.push_error("multiasset must not be empty".to_string()); + ctx.push_error("multiasset must not be empty".to_string())?; } Ok(NonEmptyMultiasset { asset }) } @@ -1118,7 +1097,7 @@ impl<'b, C> minicbor::Encode for TransactionOutput<'b> { impl<'b, Ctx> minicbor::Decode<'b, Ctx> for TransactionOutput<'b> where - Ctx: StrictContext + Ctx: ValidationContext { fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { match d.datatype()? { @@ -1252,30 +1231,6 @@ pub struct WitnessSet<'b> { pub plutus_v3_script: Option>>, } -#[derive(Debug, PartialEq, Clone)] -pub struct StrictWitnessSet<'b> { - inner: WitnessSet<'b> -} - -impl<'b, Ctx> minicbor::Decode<'b, Ctx> for StrictWitnessSet<'b> -where - Ctx: StrictContext -{ - fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut Ctx) -> Result { - let inner: WitnessSet<'b> = d.decode_with(ctx)?; - let errs = ctx.get_errors(); - if !errs.is_empty() { - let s = errs.join(";"); - return Err(minicbor::decode::Error::message( - format!("WitnessSet failed strict validations: {}", s) - )); - } - Ok(StrictWitnessSet { - inner - }) - } -} - #[deprecated(since = "1.0.0-alpha", note = "use `WitnessSet` instead")] pub type MintedWitnessSet<'b> = WitnessSet<'b>; @@ -1355,7 +1310,7 @@ pub use crate::alonzo::AuxiliaryData; /// original CBOR bytes for each structure that might require hashing. In this /// way, we make sure that the resulting hash matches what exists on-chain. #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)] -#[cbor(context_bound = "StrictContext")] +#[cbor(context_bound = "ValidationContext")] pub struct Block<'b> { #[n(0)] pub header: KeepRaw<'b, Header>, @@ -1377,7 +1332,7 @@ pub struct Block<'b> { pub type MintedBlock<'b> = Block<'b>; #[derive(Clone, Serialize, Deserialize, Encode, Decode, Debug)] -#[cbor(context_bound = "StrictContext")] +#[cbor(context_bound = "ValidationContext")] pub struct Tx<'b> { #[b(0)] pub transaction_body: KeepRaw<'b, TransactionBody<'b>>, @@ -1418,18 +1373,19 @@ mod tests { #[cfg(test)] mod tests_value { - use super::super::BasicStrictContext; - use super::super::{StrictMint}; - use super::super::{Multiasset, StrictMultiasset}; + use super::super::AccumulatingContext; + use super::super::Mint; + use super::super::Multiasset; use super::super::NonZeroInt; - use super::super::{Value, StrictValue}; + use super::super::Value; + use super::super::Strict; use pallas_codec::minicbor; use std::collections::BTreeMap; // a value can have zero coins and omit the multiasset #[test] fn decode_zero_value() { - let ma: StrictValue = minicbor::decode_with(&hex::decode("00").unwrap(), &mut BasicStrictContext::new()).unwrap(); + let ma: Strict = minicbor::decode_with(&hex::decode("00").unwrap(), &mut AccumulatingContext::new()).unwrap(); assert_eq!(ma.inner, Value::Coin(0)); } @@ -1437,24 +1393,24 @@ mod tests { // Note: this will roundtrip back to "00" #[test] fn permit_definite_value() { - let ma: StrictValue = minicbor::decode_with(&hex::decode("8200a0").unwrap(), &mut BasicStrictContext::new()).unwrap(); + let ma: Strict = minicbor::decode_with(&hex::decode("8200a0").unwrap(), &mut AccumulatingContext::new()).unwrap(); assert_eq!(ma.inner, Value::Multiasset(0, Multiasset(BTreeMap::new()))); } // Indefinite-encoded value is valid #[test] fn permit_indefinite_value() { - let ma: StrictValue = minicbor::decode_with(&hex::decode("9f00a0ff").unwrap(), &mut BasicStrictContext::new()).unwrap(); + let ma: Strict = minicbor::decode_with(&hex::decode("9f00a0ff").unwrap(), &mut AccumulatingContext::new()).unwrap(); assert_eq!(ma.inner, Value::Multiasset(0, Multiasset(BTreeMap::new()))); } // the asset sub-map of a policy map in a multiasset must not be null in Conway #[test] fn reject_null_tokens() { - let ma: Result = minicbor::decode_with(&hex::decode("8200a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap(), &mut BasicStrictContext::new()); + let ma: Result, _> = minicbor::decode_with(&hex::decode("8200a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap(), &mut AccumulatingContext::new()); assert_eq!( ma.map_err(|e| e.to_string()), - Err("decode error: Value failed strict validations: Policy must not be empty".to_owned()) + Err("decode error: Failed strict validation: Policy must not be empty".to_owned()) ); } @@ -1462,7 +1418,7 @@ mod tests { // Conway #[test] fn reject_zero_tokens() { - let ma: Result = minicbor::decode_with(&hex::decode("8200a1581c00000000000000000000000000000000000000000000000000000000a14000").unwrap(), &mut BasicStrictContext::new()); + let ma: Result, _> = minicbor::decode_with(&hex::decode("8200a1581c00000000000000000000000000000000000000000000000000000000a14000").unwrap(), &mut AccumulatingContext::new()); assert_eq!( ma.map_err(|e| e.to_string()), Err("decode error: PositiveCoin must not be 0".to_owned()) @@ -1471,10 +1427,10 @@ mod tests { #[test] fn multiasset_reject_null_tokens() { - let ma: Result, _> = minicbor::decode_with(&hex::decode("a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap(), &mut BasicStrictContext::new()); + let ma: Result>, _> = minicbor::decode_with(&hex::decode("a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap(), &mut AccumulatingContext::new()); assert_eq!( ma.map_err(|e| e.to_string()), - Err("decode error: Multiasset failed strict validations: Policy must not be empty".to_owned()) + Err("decode error: Failed strict validation: Policy must not be empty".to_owned()) ); } @@ -1492,38 +1448,38 @@ mod tests { // minimal token map (conway requires nonempty asset maps) s += "a14001"; } - let ma: Result, _> = minicbor::decode_with(&hex::decode(s).unwrap(), &mut BasicStrictContext::new()); + let ma: Result>, _> = minicbor::decode_with(&hex::decode(s).unwrap(), &mut AccumulatingContext::new()); match ma { Ok(_) => panic!("decode succeded but should fail"), - Err(e) => assert_eq!(e.to_string(), "decode error: Multiasset failed strict validations: Multiasset must not exceed size limit") + Err(e) => assert_eq!(e.to_string(), "decode error: Failed strict validation: Multiasset must not exceed size limit") } } #[test] fn mint_reject_null_tokens() { - let ma: Result = minicbor::decode_with(&hex::decode("a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap(), &mut BasicStrictContext::new()); + let ma: Result, _> = minicbor::decode_with(&hex::decode("a1581c00000000000000000000000000000000000000000000000000000000a0").unwrap(), &mut AccumulatingContext::new()); assert_eq!( ma.map_err(|e| e.to_string()), - Err("decode error: Mint failed strict validations: Policy must not be empty".to_owned()) + Err("decode error: Failed strict validation: Policy must not be empty".to_owned()) ); } } mod tests_witness_set { - use super::super::{BasicStrictContext, Bytes, VKeyWitness, WitnessSet, StrictWitnessSet}; + use super::super::{AccumulatingContext, Bytes, VKeyWitness, WitnessSet, Strict}; use pallas_codec::minicbor; #[test] fn decode_empty_witness_set() { let witness_set_bytes = hex::decode("a0").unwrap(); - let ws: WitnessSet = minicbor::decode_with(&witness_set_bytes, &mut BasicStrictContext::new()).unwrap(); + let ws: WitnessSet = minicbor::decode_with(&witness_set_bytes, &mut AccumulatingContext::new()).unwrap(); assert_eq!(ws.vkeywitness, None); } #[test] fn decode_witness_set_having_vkeywitness_untagged_must_be_nonempty() { let witness_set_bytes = hex::decode("a10080").unwrap(); - let ws: Result = minicbor::decode_with(&witness_set_bytes, &mut BasicStrictContext::new()); + let ws: Result, _> = minicbor::decode_with(&witness_set_bytes, &mut AccumulatingContext::new()); assert_eq!( ws.map_err(|e| e.to_string()), Err("decode error: decoding empty set as NonEmptySet".to_owned()) @@ -1533,7 +1489,7 @@ mod tests { #[test] fn decode_witness_set_having_vkeywitness_untagged_singleton() { let witness_set_bytes = hex::decode("a10081824040").unwrap(); - let ws: StrictWitnessSet = minicbor::decode_with(&witness_set_bytes, &mut BasicStrictContext::new()).unwrap(); + let ws: Strict = minicbor::decode_with(&witness_set_bytes, &mut AccumulatingContext::new()).unwrap(); let expected = VKeyWitness { vkey: Bytes::from(vec![]), @@ -1545,7 +1501,7 @@ mod tests { #[test] fn decode_witness_set_having_vkeywitness_conwaystyle_singleton() { let witness_set_bytes = hex::decode("a100d9010281824040").unwrap(); - let ws: StrictWitnessSet = minicbor::decode_with(&witness_set_bytes, &mut BasicStrictContext::new()).unwrap(); + let ws: Strict = minicbor::decode_with(&witness_set_bytes, &mut AccumulatingContext::new()).unwrap(); let expected = VKeyWitness { vkey: Bytes::from(vec![]), @@ -1557,7 +1513,7 @@ mod tests { #[test] fn decode_witness_set_having_vkeywitness_conwaystyle_must_be_nonempty() { let witness_set_bytes = hex::decode("a100d9010280").unwrap(); - let ws: Result = minicbor::decode_with(&witness_set_bytes, &mut BasicStrictContext::new()); + let ws: Result, _> = minicbor::decode_with(&witness_set_bytes, &mut AccumulatingContext::new()); assert_eq!( ws.map_err(|e| e.to_string()), Err("decode error: decoding empty set as NonEmptySet".to_owned()) @@ -1568,7 +1524,7 @@ mod tests { fn decode_witness_set_having_vkeywitness_reject_nonsense_tag() { // VKey witness set with nonsense tag 259 let witness_set_bytes = hex::decode("a100d9010381824040").unwrap(); - let ws: Result = minicbor::decode_with(&witness_set_bytes, &mut BasicStrictContext::new()); + let ws: Result, _> = minicbor::decode_with(&witness_set_bytes, &mut AccumulatingContext::new()); assert_eq!( ws.map_err(|e| e.to_string()), Err("decode error: Unrecognised tag: Tag(259)".to_owned()) @@ -1583,7 +1539,7 @@ mod tests { #[test] fn decode_witness_set_having_vkeywitness_duplicate_entries() { let witness_set_bytes = hex::decode("a100d9010282824040824040").unwrap(); - let ws: StrictWitnessSet = minicbor::decode_with(&witness_set_bytes, &mut BasicStrictContext::new()).unwrap(); + let ws: Strict = minicbor::decode_with(&witness_set_bytes, &mut AccumulatingContext::new()).unwrap(); let expected = VKeyWitness { vkey: Bytes::from(vec![]), @@ -1646,7 +1602,7 @@ mod tests { } mod tests_transaction { - use super::super::{BasicStrictContext, TransactionBody, StrictTransactionBody}; + use super::super::{AccumulatingContext, TransactionBody, Strict}; use pallas_codec::minicbor; // A simple tx with just inputs, outputs, and fee. Address is not well-formed, since the @@ -1654,7 +1610,7 @@ mod tests { #[test] fn decode_simple_tx() { let tx_bytes = hex::decode("a300828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a04000000").unwrap(); - let tx: StrictTransactionBody = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()).unwrap(); + let tx: Strict = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()).unwrap(); let tx: TransactionBody = tx.inner; assert_eq!(tx.fee, 0); } @@ -1664,7 +1620,7 @@ mod tests { #[test] fn reject_empty_tx() { let tx_bytes = hex::decode("a0").unwrap(); - let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); + let tx: Result>, _> = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), Err("decode error: inputs, outputs, and fee fields are required".to_owned()) @@ -1675,7 +1631,7 @@ mod tests { #[test] fn reject_tx_missing_outputs() { let tx_bytes = hex::decode("a200818258200000000000000000000000000000000000000000000000000000000000000008090200").unwrap(); - let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); + let tx: Result>, _> = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), Err("decode error: inputs, outputs, and fee fields are required".to_owned()) @@ -1686,7 +1642,7 @@ mod tests { #[test] fn reject_tx_missing_fee() { let tx_bytes = hex::decode("a20081825820000000000000000000000000000000000000000000000000000000000000000809018182581c000000000000000000000000000000000000000000000000000000001affffffff").unwrap(); - let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); + let tx: Result>, _> = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), Err("decode error: inputs, outputs, and fee fields are required".to_owned()) @@ -1699,17 +1655,17 @@ mod tests { #[test] fn reject_empty_present_mint() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a0400000009a0").unwrap(); - let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); + let tx: Result>, _> = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), - Err("decode error: TransactionBody failed strict validations: multiasset must not be empty".to_owned()) + Err("decode error: Failed strict validation: multiasset must not be empty".to_owned()) ); } #[test] fn reject_empty_present_certs() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000000480").unwrap(); - let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); + let tx: Result>, _> = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), Err("decode error: decoding empty set as NonEmptySet".to_owned()) @@ -1719,17 +1675,17 @@ mod tests { #[test] fn reject_empty_present_withdrawals() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a0400000005a0").unwrap(); - let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); + let tx: Result>, _> = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), - Err("decode error: TransactionBody failed strict validations: map must not be empty".to_owned()) + Err("decode error: Failed strict validation: map must not be empty".to_owned()) ); } #[test] fn reject_empty_present_collateral_inputs() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000000d80").unwrap(); - let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); + let tx: Result>, _> = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), Err("decode error: decoding empty set as NonEmptySet".to_owned()) @@ -1739,7 +1695,7 @@ mod tests { #[test] fn reject_empty_present_required_signers() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000000e80").unwrap(); - let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); + let tx: Result>, _> = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), Err("decode error: decoding empty set as NonEmptySet".to_owned()) @@ -1749,17 +1705,17 @@ mod tests { #[test] fn reject_empty_present_voting_procedures() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a0400000013a0").unwrap(); - let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); + let tx: Result>, _> = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), - Err("decode error: TransactionBody failed strict validations: map must not be empty".to_owned()) + Err("decode error: Failed strict validation: map must not be empty".to_owned()) ); } #[test] fn reject_empty_present_proposal_procedures() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000001480").unwrap(); - let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); + let tx: Result>, _> = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), Err("decode error: decoding empty set as NonEmptySet".to_owned()) @@ -1769,7 +1725,7 @@ mod tests { #[test] fn reject_empty_present_donation() { let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a040000001600").unwrap(); - let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); + let tx: Result>, _> = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), Err("decode error: PositiveCoin must not be 0".to_owned()) @@ -1780,10 +1736,10 @@ mod tests { #[test] fn reject_duplicate_keys() { let tx_bytes = hex::decode("a40081825820000000000000000000000000000000000000000000000000000000000000000809018182581c000000000000000000000000000000000000000000000000000000001affffffff02010201").unwrap(); - let tx: Result, _> = minicbor::decode_with(&tx_bytes, &mut BasicStrictContext::new()); + let tx: Result>, _> = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()); assert_eq!( tx.map_err(|e| e.to_string()), - Err("decode error: TransactionBody failed strict validations: duplicate txbody entries for key 2".to_owned()) + Err("decode error: Failed strict validation: duplicate txbody entries for key 2".to_owned()) ); } }