diff --git a/pallas-codec/src/utils.rs b/pallas-codec/src/utils.rs index cb2eec8c1..a74a1f333 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 { @@ -806,6 +806,35 @@ impl NonEmptySet { } } +pub trait BusinessInvariants { + /// Checks that the business invariants for this type hold. + 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/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..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; @@ -49,7 +50,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 +153,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 +196,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 +272,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 +286,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, @@ -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 { @@ -398,7 +410,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 +422,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 +453,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 +518,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, @@ -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 03269ff5c..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; @@ -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..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,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)?; 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,14 @@ 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) -> ValidationResult { + if mtx.validate_data() { + 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>;