From 7a5069749fb0fe216030c730eadd9a7644d7cb6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20S=C3=A1nchez=20Terraf?= Date: Mon, 14 Apr 2025 15:33:36 -0300 Subject: [PATCH 1/2] WIP validation of (decoded) data --- pallas-codec/src/utils.rs | 14 ++++++++++++-- pallas-primitives/src/alonzo/model.rs | 6 +++--- pallas-primitives/src/conway/model.rs | 18 +++++++++--------- pallas-primitives/src/lib.rs | 10 +++++----- pallas-validate/src/phase1/conway.rs | 24 ++++++++++++++++++++++++ pallas-validate/src/utils/validation.rs | 3 +++ 6 files changed, 56 insertions(+), 19 deletions(-) diff --git a/pallas-codec/src/utils.rs b/pallas-codec/src/utils.rs index d8ab26c88..b03f9a4e7 100644 --- a/pallas-codec/src/utils.rs +++ b/pallas-codec/src/utils.rs @@ -6,7 +6,7 @@ use minicbor::{ use serde::{Deserialize, Serialize}; use std::{borrow::Cow, str::FromStr}; use std::{ - collections::HashMap, + collections::{HashMap, BTreeSet}, fmt, hash::Hash as StdHash, ops::{Deref, DerefMut}, @@ -712,7 +712,7 @@ where /// /// Optional 258 tag (until era after Conway, at which point is it required) /// with a vec of items which should contain no duplicates -#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Serialize, Deserialize, Ord)] pub struct Set(Vec); impl Set { @@ -804,6 +804,16 @@ impl NonEmptySet { Some(Self(x)) } } + + /// Checks that the business invariants for this type hold. + /// In this casem that means the the inner vec is nonempty and contains no duplicates. + pub fn validate_data(&self) -> bool + where + T: Ord, + { + let mut seen = BTreeSet::new(); + !self.0.is_empty() && self.0.iter().all(|item| seen.insert(item)) + } } impl Deref for NonEmptySet { diff --git a/pallas-primitives/src/alonzo/model.rs b/pallas-primitives/src/alonzo/model.rs index 9e4c32bfc..ff9c0d652 100644 --- a/pallas-primitives/src/alonzo/model.rs +++ b/pallas-primitives/src/alonzo/model.rs @@ -314,7 +314,7 @@ pub struct TransactionBody { pub network_id: Option, } -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct VKeyWitness { #[n(0)] pub vkey: Bytes, @@ -323,7 +323,7 @@ pub struct VKeyWitness { pub signature: Bytes, } -#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] #[cbor(flat)] pub enum NativeScript { #[n(0)] @@ -384,7 +384,7 @@ pub struct RedeemerPointer { , attributes : bytes ] */ -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct BootstrapWitness { #[n(0)] pub public_key: Bytes, diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index 2d84fb862..8c9175f39 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -49,7 +49,7 @@ pub type Withdrawals = BTreeMap; pub type RequiredSigners = NonEmptySet; -#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] #[cbor(flat)] pub enum Certificate { #[n(0)] @@ -152,7 +152,7 @@ pub enum Language { #[deprecated(since = "0.31.0", note = "use `CostModels` instead")] pub type CostMdls = CostModels; -#[derive(Serialize, Deserialize, Encode, Debug, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize, Encode, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] #[cbor(map)] pub struct CostModels { #[n(0)] @@ -195,7 +195,7 @@ impl<'b, C> minicbor::Decode<'b, C> for CostModels { } } -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] #[cbor(map)] pub struct ProtocolParamUpdate { #[n(0)] @@ -271,7 +271,7 @@ pub struct Update { pub epoch: Epoch, } -#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct PoolVotingThresholds { #[n(0)] pub motion_no_confidence: UnitInterval, @@ -285,7 +285,7 @@ pub struct PoolVotingThresholds { pub security_voting_threshold: UnitInterval, } -#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct DRepVotingThresholds { #[n(0)] pub motion_no_confidence: UnitInterval, @@ -398,7 +398,7 @@ pub struct VotingProcedure { pub anchor: Option, } -#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct ProposalProcedure { #[n(0)] pub deposit: Coin, @@ -410,7 +410,7 @@ pub struct ProposalProcedure { pub anchor: Anchor, } -#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] #[cbor(flat)] pub enum GovAction { #[n(0)] @@ -441,7 +441,7 @@ pub enum GovAction { Information, } -#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct Constitution { #[n(0)] pub anchor: Anchor, @@ -506,7 +506,7 @@ pub use crate::alonzo::VKeyWitness; pub use crate::alonzo::NativeScript; -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct ExUnitPrices { #[n(0)] pub mem_price: RationalNumber, diff --git a/pallas-primitives/src/lib.rs b/pallas-primitives/src/lib.rs index 03269ff5c..28d23ac4e 100644 --- a/pallas-primitives/src/lib.rs +++ b/pallas-primitives/src/lib.rs @@ -40,7 +40,7 @@ pub type DnsName = String; pub type Epoch = u64; -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] pub struct ExUnits { #[n(0)] pub mem: u64, @@ -139,7 +139,7 @@ pub enum NonceVariant { Nonce, } -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] #[cbor(transparent)] pub struct PlutusScript(pub Bytes); @@ -153,7 +153,7 @@ pub type PolicyId = Hash<28>; pub type PoolKeyhash = Hash<28>; -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct PoolMetadata { #[n(0)] pub url: String, @@ -170,7 +170,7 @@ pub type PositiveInterval = RationalNumber; pub type ProtocolVersion = (u64, u64); -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct RationalNumber { pub numerator: u64, pub denominator: u64, @@ -202,7 +202,7 @@ impl minicbor::encode::Encode for RationalNumber { } } -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub enum Relay { SingleHostAddr(Option, Option, Option), SingleHostName(Option, DnsName), diff --git a/pallas-validate/src/phase1/conway.rs b/pallas-validate/src/phase1/conway.rs index 6f4dbc185..fea4536eb 100644 --- a/pallas-validate/src/phase1/conway.rs +++ b/pallas-validate/src/phase1/conway.rs @@ -38,6 +38,7 @@ pub fn validate_conway_tx( ) -> ValidationResult { let tx_body: &TransactionBody = &mtx.transaction_body.clone(); let size: u32 = get_conway_tx_size(mtx).ok_or(PostAlonzo(UnknownTxSize))?; + check_data_business_invariants(mtx, tx_body)?; check_ins_not_empty(tx_body)?; check_all_ins_in_utxos(tx_body, utxos)?; check_tx_validity_interval(tx_body, block_slot)?; @@ -56,6 +57,29 @@ pub fn validate_conway_tx( check_script_data_hash(tx_body, mtx, utxos, network_magic, network_id, block_slot) } +fn check_data_business_invariants(mtx: &Tx, tx_body: &TransactionBody) -> ValidationResult { + let witness_set: &WitnessSet = &mtx.transaction_witness_set; + let body_invariants: bool = + tx_body.certificates.as_ref().is_none_or(|x| x.validate_data()) && + tx_body.collateral.as_ref().is_none_or(|x| x.validate_data()) && + tx_body.required_signers.as_ref().is_none_or(|x| x.validate_data()) && + tx_body.reference_inputs.as_ref().is_none_or(|x| x.validate_data()) && + tx_body.proposal_procedures.as_ref().is_none_or(|x| x.validate_data()); + let witness_invariants: bool = + witness_set.vkeywitness.as_ref().is_none_or(|x| x.validate_data()) && + witness_set.native_script.as_ref().is_none_or(|x| x.validate_data()) && + witness_set.bootstrap_witness.as_ref().is_none_or(|x| x.validate_data()) && + witness_set.plutus_v1_script.as_ref().is_none_or(|x| x.validate_data()) && + witness_set.plutus_data.as_ref().is_none_or(|x| x.validate_data()) && + witness_set.plutus_v2_script.as_ref().is_none_or(|x| x.validate_data()) && + witness_set.plutus_v3_script.as_ref().is_none_or(|x| x.validate_data()); + if body_invariants && witness_invariants { + Ok(()) + } else { + Err(PostAlonzo(BrokenBusinessInvariant)) + } +} + // The set of transaction inputs is not empty. fn check_ins_not_empty(tx_body: &TransactionBody) -> ValidationResult { if tx_body.inputs.is_empty() { diff --git a/pallas-validate/src/utils/validation.rs b/pallas-validate/src/utils/validation.rs index 35816739e..491fdac60 100644 --- a/pallas-validate/src/utils/validation.rs +++ b/pallas-validate/src/utils/validation.rs @@ -409,6 +409,9 @@ pub enum PostAlonzoError { #[error("invalid script integrity hash")] ScriptIntegrityHash, + + #[error("transaction data does not satisfy business/CDDL invariant")] + BrokenBusinessInvariant, } pub type ValidationResult = Result<(), ValidationError>; From fb213432a19214e4ede36f1642205a9dfea7256b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20S=C3=A1nchez=20Terraf?= Date: Mon, 14 Apr 2025 16:24:22 -0300 Subject: [PATCH 2/2] Business invariants as a trait --- pallas-codec/src/utils.rs | 27 ++++++++++++++++++---- pallas-primitives/src/conway/model.rs | 33 +++++++++++++++++++++++++++ pallas-primitives/src/lib.rs | 2 +- pallas-validate/src/phase1/conway.rs | 23 ++++--------------- 4 files changed, 61 insertions(+), 24 deletions(-) diff --git a/pallas-codec/src/utils.rs b/pallas-codec/src/utils.rs index b03f9a4e7..d748d36f7 100644 --- a/pallas-codec/src/utils.rs +++ b/pallas-codec/src/utils.rs @@ -804,18 +804,37 @@ impl NonEmptySet { Some(Self(x)) } } +} +pub trait BusinessInvariants { /// Checks that the business invariants for this type hold. - /// In this casem that means the the inner vec is nonempty and contains no duplicates. - pub fn validate_data(&self) -> bool - where - T: Ord, + fn validate_data(&self) -> bool; +} + +impl BusinessInvariants for Option { + fn validate_data(&self) -> bool + { + self.as_ref().is_none_or(|x| x.validate_data()) + } +} + +impl BusinessInvariants for &NonEmptySet { + /// The inner vec is nonempty and contains no duplicates. + fn validate_data(&self) -> bool { let mut seen = BTreeSet::new(); !self.0.is_empty() && self.0.iter().all(|item| seen.insert(item)) } } +impl BusinessInvariants for NonEmptySet { + /// The inner vec is nonempty and contains no duplicates. + fn validate_data(&self) -> bool + { + (&self).validate_data() + } +} + impl Deref for NonEmptySet { type Target = Vec; diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index 8c9175f39..73b2250f4 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -15,6 +15,7 @@ pub use crate::{ PlutusScript, PolicyId, PoolKeyhash, PoolMetadata, PoolMetadataHash, Port, PositiveCoin, PositiveInterval, ProtocolVersion, RationalNumber, Relay, RewardAccount, ScriptHash, Set, StakeCredential, TransactionIndex, TransactionInput, UnitInterval, VrfCert, VrfKeyhash, + BusinessInvariants, }; use crate::BTreeMap; @@ -377,6 +378,17 @@ pub struct TransactionBody<'a> { #[deprecated(since = "1.0.0-alpha", note = "use `TransactionBody` instead")] pub type MintedTransactionBody<'a> = TransactionBody<'a>; +impl BusinessInvariants for TransactionBody<'_> { + fn validate_data(&self) -> bool + { + self.certificates.validate_data() && + self.collateral.validate_data() && + self.required_signers.validate_data() && + self.reference_inputs.validate_data() && + self.proposal_procedures.validate_data() + } +} + #[derive(Encode, Decode, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] #[cbor(index_only)] pub enum Vote { @@ -617,6 +629,19 @@ pub struct WitnessSet<'b> { #[deprecated(since = "1.0.0-alpha", note = "use `WitnessSet` instead")] pub type MintedWitnessSet<'b> = WitnessSet<'b>; +impl BusinessInvariants for WitnessSet<'_> { + fn validate_data(&self) -> bool + { + self.vkeywitness.validate_data() && + self.native_script.validate_data() && + self.bootstrap_witness.validate_data() && + self.plutus_v1_script.validate_data() && + self.plutus_data.validate_data() && + self.plutus_v2_script.validate_data() && + self.plutus_v3_script.validate_data() + } +} + #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)] #[cbor(map)] pub struct PostAlonzoAuxiliaryData { @@ -720,6 +745,14 @@ pub struct Tx<'b> { pub auxiliary_data: Nullable>, } +impl BusinessInvariants for Tx<'_> { + fn validate_data(&self) -> bool + { + self.transaction_body.validate_data() && + self.transaction_witness_set.validate_data() + } +} + #[deprecated(since = "1.0.0-alpha", note = "use `Tx` instead")] pub type MintedTx<'b> = Tx<'b>; diff --git a/pallas-primitives/src/lib.rs b/pallas-primitives/src/lib.rs index 28d23ac4e..5301a6410 100644 --- a/pallas-primitives/src/lib.rs +++ b/pallas-primitives/src/lib.rs @@ -15,7 +15,7 @@ pub use pallas_codec::codec_by_datatype; pub use pallas_codec::utils::{ Bytes, Int, KeepRaw, KeyValuePairs, MaybeIndefArray, NonEmptySet, NonZeroInt, Nullable, - PositiveCoin, Set, + PositiveCoin, Set, BusinessInvariants, }; pub use pallas_crypto::hash::Hash; diff --git a/pallas-validate/src/phase1/conway.rs b/pallas-validate/src/phase1/conway.rs index fea4536eb..9698b9575 100644 --- a/pallas-validate/src/phase1/conway.rs +++ b/pallas-validate/src/phase1/conway.rs @@ -15,7 +15,7 @@ use crate::utils::{ use pallas_addresses::{ScriptHash, ShelleyAddress, ShelleyPaymentPart}; use pallas_codec::{ minicbor::{encode, Encoder}, - utils::{Bytes, KeepRaw}, + utils::{BusinessInvariants, Bytes, KeepRaw}, }; use pallas_primitives::{ babbage, @@ -38,7 +38,7 @@ pub fn validate_conway_tx( ) -> ValidationResult { let tx_body: &TransactionBody = &mtx.transaction_body.clone(); let size: u32 = get_conway_tx_size(mtx).ok_or(PostAlonzo(UnknownTxSize))?; - check_data_business_invariants(mtx, tx_body)?; + check_data_business_invariants(mtx)?; check_ins_not_empty(tx_body)?; check_all_ins_in_utxos(tx_body, utxos)?; check_tx_validity_interval(tx_body, block_slot)?; @@ -57,23 +57,8 @@ pub fn validate_conway_tx( check_script_data_hash(tx_body, mtx, utxos, network_magic, network_id, block_slot) } -fn check_data_business_invariants(mtx: &Tx, tx_body: &TransactionBody) -> ValidationResult { - let witness_set: &WitnessSet = &mtx.transaction_witness_set; - let body_invariants: bool = - tx_body.certificates.as_ref().is_none_or(|x| x.validate_data()) && - tx_body.collateral.as_ref().is_none_or(|x| x.validate_data()) && - tx_body.required_signers.as_ref().is_none_or(|x| x.validate_data()) && - tx_body.reference_inputs.as_ref().is_none_or(|x| x.validate_data()) && - tx_body.proposal_procedures.as_ref().is_none_or(|x| x.validate_data()); - let witness_invariants: bool = - witness_set.vkeywitness.as_ref().is_none_or(|x| x.validate_data()) && - witness_set.native_script.as_ref().is_none_or(|x| x.validate_data()) && - witness_set.bootstrap_witness.as_ref().is_none_or(|x| x.validate_data()) && - witness_set.plutus_v1_script.as_ref().is_none_or(|x| x.validate_data()) && - witness_set.plutus_data.as_ref().is_none_or(|x| x.validate_data()) && - witness_set.plutus_v2_script.as_ref().is_none_or(|x| x.validate_data()) && - witness_set.plutus_v3_script.as_ref().is_none_or(|x| x.validate_data()); - if body_invariants && witness_invariants { +fn check_data_business_invariants(mtx: &Tx) -> ValidationResult { + if mtx.validate_data() { Ok(()) } else { Err(PostAlonzo(BrokenBusinessInvariant))