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 54c1a742a..d47fd115e 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -27,7 +27,54 @@ 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 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)?; + + // 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 +84,44 @@ 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, + 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 +392,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 +457,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 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()?; + } 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>; @@ -680,6 +938,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 @@ -726,6 +991,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.0.len(); + let mut asset_count = 0; + for (_policy, assets) in &ma.0 { + 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; @@ -733,6 +1012,376 @@ 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_value() { + let ma: Value = minicbor::decode(&hex::decode("8200a0").unwrap()).unwrap(); + 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, 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()); + assert_eq!( + ma.map_err(|e| e.to_string()), + Err("decode error: Policy must not be empty".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("decode error: PositiveCoin must not be 0".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("decode error: Policy must not be empty".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() { + // 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(), "decode error: Multiasset must not exceed size limit") + } + } + + #[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("decode error: Policy must not be empty".to_owned()) + ); + } + } + + 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_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_untagged_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("decode error: decoding empty set as NonEmptySet".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. + // 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(); + 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.clone(), expected])); + } + + } + + mod tests_auxdata { + 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: AuxiliaryData = + minicbor::decode(&auxdata_bytes).unwrap(); + 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: AuxiliaryData = + minicbor::decode(&auxdata_bytes).unwrap(); + 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: AuxiliaryData = + minicbor::decode(&auxdata_bytes).unwrap(); + match auxdata { + AuxiliaryData::PostAlonzo(a) => { + assert_eq!(a.metadata, None); + } + _ => { + panic!("Unexpected variant"); + } + } + } + } + + mod tests_transaction { + use super::super::TransactionBody; + use pallas_codec::minicbor; + + // 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(); + 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("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()) + ); + } + + // 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(); + let tx: Result, _> = minicbor::decode(&tx_bytes); + assert_eq!( + tx.map_err(|e| e.to_string()), + 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)] mod tests_voter { use super::super::Voter;