diff --git a/pallas-codec/src/utils.rs b/pallas-codec/src/utils.rs index cb2eec8c..988e1966 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 54c1a742..f33d78d8 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -27,9 +27,167 @@ pub use crate::babbage::OperationalCert; pub use crate::babbage::Header; -pub type Multiasset = BTreeMap>; +use pallas_codec::minicbor::data::Type; -pub type Mint = Multiasset; +trait ValidationContext { + fn push_error(&mut self, s: String) -> Result<(), minicbor::decode::Error>; + fn get_errors(&self) -> &[String]; +} + +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 AccumulatingContext { + pub fn new() -> Self { + AccumulatingContext { + errors: vec![] + } + } +} + +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 + } +} + +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>); + +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, Ctx, A: minicbor::Decode<'b, Ctx>> minicbor::Decode<'b, Ctx> for Multiasset +where + Ctx: ValidationContext +{ + 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 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) { + ctx.push_error("Multiasset must not exceed size limit".to_string())?; + } + Ok(result) + } +} + +pub type Mint = NonEmptyMultiasset; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub enum Value { @@ -37,15 +195,52 @@ 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, Ctx> minicbor::Decode<'b, Ctx> for Value +where + Ctx: ValidationContext +{ + 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_with(ctx)?; + Ok(Value::Coin(coin)) + } + Type::Array | Type::ArrayIndef => { + let _ = d.array()?; + let coin = d.decode_with(ctx)?; + let multiasset = d.decode_with(ctx)?; + Ok(Value::Multiasset(coin, multiasset)) + } + t => { + Err(minicbor::decode::Error::message(format!("Unexpected datatype {}", t))) + } + } + } } pub use crate::alonzo::TransactionOutput as LegacyTransactionOutput; -pub type Withdrawals = BTreeMap; +pub type Withdrawals = NonEmptyMap; pub type RequiredSigners = NonEmptySet; @@ -311,8 +506,7 @@ pub struct DRepVotingThresholds { pub treasury_withdrawal: UnitInterval, } -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)] -#[cbor(map)] +#[derive(Encode, Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct TransactionBody<'a> { #[n(0)] pub inputs: Set, @@ -330,7 +524,7 @@ pub struct TransactionBody<'a> { pub certificates: Option>, #[n(5)] - pub withdrawals: Option>, + pub withdrawals: Option>, #[n(7)] pub auxiliary_data_hash: Option, @@ -339,7 +533,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>, @@ -376,6 +570,395 @@ pub struct TransactionBody<'a> { pub donation: Option, } +#[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: ValidationContext +{ + 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: ValidationContext +{ + 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; + } + 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]); + } + }, + 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 + }) + } +} + +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(()) +} + +impl <'b, Ctx> minicbor::Decode<'b, Ctx> for TransactionBody<'b> +where + Ctx: ValidationContext +{ + 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) + } +} + +#[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: 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())?; + } + 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: 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())?; + } + 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>; @@ -390,7 +973,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 { @@ -493,12 +1076,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: ValidationContext +{ + 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")] @@ -680,6 +1295,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 @@ -688,6 +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 = "ValidationContext")] pub struct Block<'b> { #[n(0)] pub header: KeepRaw<'b, Header>, @@ -709,6 +1332,7 @@ pub struct Block<'b> { pub type MintedBlock<'b> = Block<'b>; #[derive(Clone, Serialize, Deserialize, Encode, Decode, Debug)] +#[cbor(context_bound = "ValidationContext")] pub struct Tx<'b> { #[b(0)] pub transaction_body: KeepRaw<'b, TransactionBody<'b>>, @@ -726,6 +1350,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 assets in ma.0.values() { + 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 +1371,379 @@ mod tests { type BlockWrapper<'b> = (u16, Block<'b>); + #[cfg(test)] + mod tests_value { + use super::super::AccumulatingContext; + use super::super::Mint; + use super::super::Multiasset; + use super::super::NonZeroInt; + 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: Strict = minicbor::decode_with(&hex::decode("00").unwrap(), &mut AccumulatingContext::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: 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: 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 AccumulatingContext::new()); + assert_eq!( + ma.map_err(|e| e.to_string()), + Err("decode error: Failed strict validation: 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_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()) + ); + } + + #[test] + fn multiasset_reject_null_tokens() { + 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: Failed strict validation: 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_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: 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 AccumulatingContext::new()); + assert_eq!( + ma.map_err(|e| e.to_string()), + Err("decode error: Failed strict validation: Policy must not be empty".to_owned()) + ); + } + } + + mod tests_witness_set { + 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 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 AccumulatingContext::new()); + 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: Strict = minicbor::decode_with(&witness_set_bytes, &mut AccumulatingContext::new()).unwrap(); + + let expected = VKeyWitness { + vkey: Bytes::from(vec![]), + signature: Bytes::from(vec![]), + }; + 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: Strict = minicbor::decode_with(&witness_set_bytes, &mut AccumulatingContext::new()).unwrap(); + + let expected = VKeyWitness { + vkey: Bytes::from(vec![]), + signature: Bytes::from(vec![]), + }; + 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_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()) + ); + } + + #[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_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()) + ); + } + + // 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: Strict = minicbor::decode_with(&witness_set_bytes, &mut AccumulatingContext::new()).unwrap(); + + let expected = VKeyWitness { + vkey: Bytes::from(vec![]), + signature: Bytes::from(vec![]), + }; + assert_eq!(ws.inner.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::{AccumulatingContext, TransactionBody, Strict}; + 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: Strict = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()).unwrap(); + let tx: TransactionBody = tx.inner; + 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_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()) + ); + } + + // 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_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()) + ); + } + + // Single input, single output, no fee + #[test] + fn reject_tx_missing_fee() { + let tx_bytes = hex::decode("a20081825820000000000000000000000000000000000000000000000000000000000000000809018182581c000000000000000000000000000000000000000000000000000000001affffffff").unwrap(); + 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()) + ); + } + + // 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_with(&tx_bytes, &mut AccumulatingContext::new()); + assert_eq!( + tx.map_err(|e| e.to_string()), + 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 AccumulatingContext::new()); + 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_with(&tx_bytes, &mut AccumulatingContext::new()); + assert_eq!( + tx.map_err(|e| e.to_string()), + 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 AccumulatingContext::new()); + 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_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()) + ); + } + + #[test] + fn reject_empty_present_voting_procedures() { + let tx_bytes = hex::decode("a400828258206767676767676767676767676767676767676767676767676767676767676767008258206767676767676767676767676767676767676767676767676767676767676767000200018182581c000000000000000000000000000000000000000000000000000000001a0400000013a0").unwrap(); + let tx: Result>, _> = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()); + assert_eq!( + tx.map_err(|e| e.to_string()), + 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 AccumulatingContext::new()); + 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_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()) + ); + } + + + #[test] + fn reject_duplicate_keys() { + let tx_bytes = hex::decode("a40081825820000000000000000000000000000000000000000000000000000000000000000809018182581c000000000000000000000000000000000000000000000000000000001affffffff02010201").unwrap(); + let tx: Result>, _> = minicbor::decode_with(&tx_bytes, &mut AccumulatingContext::new()); + assert_eq!( + tx.map_err(|e| e.to_string()), + Err("decode error: Failed strict validation: duplicate txbody entries for key 2".to_owned()) + ); + } + } + #[cfg(test)] mod tests_voter { use super::super::Voter; diff --git a/pallas-txbuilder/src/conway.rs b/pallas-txbuilder/src/conway.rs index f99b9b00..55cadc2f 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 f58678eb..802a0ab6 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) {