diff --git a/pallas-applying/docs/shelleyMA-validation-rules.md b/pallas-applying/docs/shelleyMA-validation-rules.md new file mode 100644 index 0000000..961383b --- /dev/null +++ b/pallas-applying/docs/shelleyMA-validation-rules.md @@ -0,0 +1,108 @@ +# ShelleyMA transaction validation rules + +This document covers the Shelley era, including its Allegra and Mary hard forks. We write *ShelleyMA* to refer to any of these ledger versions, and *Shelley*, *Allegra* or *Mary* when the discrimination is relevant. This document covers only the concepts, notation and validation rules realted to phase-1 validation in these ledger versions. For further information, refer to the corresponding white paper listed below: +- [Shelley's ledger white paper](https://github.com/input-output-hk/cardano-ledger/releases/latest/download/shelley-ledger.pdf) +- [Both Allegra's and Mary's ledger white paper](https://github.com/input-output-hk/cardano-ledger/releases/latest/download/mary-ledger.pdf) + +## Definitions and notation +- **Scripts**: + - ***Script*** is the set of all possible native scripts. +- **Transactions**: + - ***Tx*** is the set of ShelleyMA transactions, composed of a transaction body and the set of witnesses. + - ***TxBody*** is the type of ShelleyMA transaction bodies. Each transaction body is composed of a set of inputs and a list of outputs. + - ***txBody(tx)*** is the transaction body of the transaction. + - ***TxOut = Addr x TA*** is the set of transaction outputs, where + - ***Addr*** is the set of transaction output addresses. + - ***TA = ℕ*** in Shelley and Allegra, while ***TA = Value*** in Mary, where ***Value*** is the type of multi-asset Mary values. + - ***txOuts(txBody) ∈ P(TxOut)*** gives the set of transaction outputs of a transaction body. + - ***balance : P(TxOut) → TA*** gives the sum of all lovelaces in a set of transaction outputs in Shelley and Allegra, while it gives the sum of all assets in a set of transaction outputs in Mary. That is, ***TA = ℕ*** in Shelley and Allegra, and ***TA = Value*** in Mary. + - ***TxIn = TxId x Ix*** is the set of transaction inputs, where + - ***TxId*** is the set of transaction IDs. + - ***Ix = ℕ*** is the set of indices (used to refer to a specific transaction output). + - ***txIns(txBody) ∈ P(TxIn)*** gives the set of transaction inputs of the transaction. + - ***utxo : TxIn → TxOut*** is a (partial) map that gives the unspent transaction output (UTxO) associated with a transaction input. + - Given ***A ⊆ dom(utxo)***, we will write ***A ◁ utxo := {to ∈ TxOut / ∃ ti ∈ dom utxo: utxo(ti) = to}***. Thus, we will write ***txIns(tx) ◁ utxo := {to ∈ TxOut / ∃ ti ∈ dom(utxo): utxo(ti) = to}*** to express the set of unspent transaction outputs associated with the set of transaction inputs of the transaction ***tx***. + - ***txTTL(txBody) ∈ Slot*** is the time-to-live of the transaction. + - ***txSize(Tx) ∈ ℕ*** gives the size of the transaction. + - ***fee(txBody) ∈ ℕ*** gives the fee paid by a transaction. + - ***minted(txBody)*** is the multiasset value minted (or burned) in the transaction. + - ***txInsScript(txBody) ⊆ P(TxIn)*** is the list of script inputs in the transaction body. + - ***consumed(pps, utxo, txBody) ∈ ℤ*** is the *consumed value* of the transaction. + - In Shelley and Allegra, this equals the sum of all lovelace in the transaction inputs. + - In Mary, this equals the sum of all multiasset values in the transaction inputs. + - ***produced(pps, txBody) ∈ ℤ*** is the *produced value* of the transaction. + - In Shelley and Allegra, this equals the sum of all lovelace in the transaction outputs plus the transaction fee. + - In Mary, this equals the sum of all multiasset values in the outputs of the transaction plus the transaction fee plus the minted value. + - **Transaction metadata**: + - ***txMD(tx)*** is the metadata of the transaction. + - ***txMDHash(txBody)*** is the metadata hash contained within the transaction body. + - ***hashMD(md)*** is the result of hasing metadata ***md***. +- **Addresses*: + - ***Addr*** is the set of all valid ShelleyMA addresses. + - ***netId(addr)*** is the network ID of the address. + - ***NetworkId*** is the global network ID. +- ***Slots***: + - ***Slot ∈ ℕ*** is the set of slots. When necessary, we write ***slot ∈ Slot*** to refer to the slot associated to the current block. +- **Serialization**: + - ***Bytes*** is the set of byte arrays (a.k.a. data, upon which signatures are built). + - ***⟦_⟧A : A -> Bytes*** takes an element of type ***A*** and returns a byte array resulting from serializing it. +- **Hashing**: + - ***KeyHash ⊆ Bytes*** is the set of fixed-size byte arrays resulting from hashing processes. + - ***hash: Bytes -> KeyHash*** is the hashing function. + - ***paymentCredentialutxo(txIn) ∈ KeyHash*** gets from ***txIn*** the associated transaction output in ***utxo***, extracts the address contained in it, and returns its hash. In other words, given ***utxo*** and transaction input ***txIn*** such that ***utxo(txIn) = (a, _)***, we have that ***paymentCredentialutxo(txIn) = hash(a)***. +- **Protocol Parameters**: + - We will write ***pps ∈ PParams*** to represent the set of (ShelleyMA) protocol parameters, with the following associated functions: + - ***minFees(pps, txBody) ∈ ℕ*** gives the minimum number of lovelace that must be paid for the transaction as fee. + - ***maxTxSize(pps) ∈ ℕ*** gives the (global) maximum transaction size. + - ***minUTxOValue(pps) ∈ ℕ***, the global minimum number of lovelace every UTxO must lock. +- ***Witnesses***: + - ***VKey*** is the set of verification keys (a.k.a. public keys). + - ***SKey*** is the set of signing keys (a.k.a. private keys). + - ***Sig*** is the set of signatures (i.e., the result of signing a byte array using a signing key). + - ***sig : SKey x Bytes -> Sig*** is the signing function. + - ***verify : VKey x Sig x Bytes -> Bool*** assesses whether the verification key applied to the signature yields the byte array as expected. + - The assumption is that if ***sk*** and ***vk*** are, respectively, a pair of secret and verification keys associated with one another. Thus, if ***sig(sk, d) = σ***, then it must be that ***verify(vk, σ, d) = true***. + - ***txVKWits(tx) ∈ P(VKey x Sig)*** gives the list of pairs of verification keys and signatures of the transaction. + - ***txScriptWits(tx) ⊆ P(Script)*** is the set of script witnesses of the transaction. + +## Validation rules +Let ***tx ∈ Tx*** be a ShelleyMA transaction whose body is ***txBody ∈ TxBody***. ***tx*** is a phase-1 valid transaction if and only if + +- **The set of transaction inputs is not empty**: + + txIns(txBody) ≠ ∅ +- **All transaction inputs are in the set of (yet) unspent transaction outputs**: + + txIns(txBody) ⊆ dom(utxo) +- **The TTL limit of the transaction has not been exceeded**: + + slot ≥ txTTL(txBody) +- **The transaction size does not exceed the protocol limit**: + + txSize(tx) ≤ maxTxSize(pps) +- **All transaction outputs contain Lovelace values not under the minimum**: + + ∀ (_, c) ∈ txOuts(txBody): minUTxOValue(pps) ≤ c +- **The preservation of value property holds**: Assuming no staking or delegation actions are involved, this property takes one of the two forms below: + - In Shelley and Allegra, the equation for the preservation of value is + + consumed(pps, utxo, txBody) = produced(pps, poolParams, txBody) + fee(txBody), + - In Mary, the equation is: + + consumed(pps, utxo, txBody) = produced(pps, poolParams, txBody) + fee(txBody) + minted(txBody) , +- **The fee paid by the transaction has to be greater than or equal to the minimum fee**: + + fee(txBody) ≥ minFees(pps, tx) +- **The network ID of each output matches the global network ID**: + + ∀(_ -> (a, _)) ∈ txOuts(txBody): netId(a) = NetworkId +- **The metadata of the transaction is valid**: + + txMDHash(tx) = hashMD(txMD(tx)) +- **Verification-key witnesses**: The owner of each transaction input signed the transaction. That is, given transaction ***tx*** with body ***txBody***, then for each ***txIn ∈ txIns(txBody)*** there must exist ***(vk, σ) ∈ txVKWits(tx)*** such that: + + - verify(vk, σ, ⟦txBody⟧TxBody) + - paymentCredentialutxo(txIn) = hash(vk) +- **Script witnesses**: Each script address has a corresponding witness: + + ∀ (script_hash, _) ∈ txInsScript(txBody) ◁ utxo : ∃ script ∈ txScriptWits(tx): hash(script) = script_hash diff --git a/pallas-applying/src/byron.rs b/pallas-applying/src/byron.rs index 0818da2..f0311ed 100644 --- a/pallas-applying/src/byron.rs +++ b/pallas-applying/src/byron.rs @@ -3,7 +3,9 @@ use std::borrow::Cow; use crate::types::{ - ByronProtParams, MultiEraInput, MultiEraOutput, SigningTag, UTxOs, ValidationError, + ByronError::*, + ByronProtParams, MultiEraInput, MultiEraOutput, SigningTag, UTxOs, + ValidationError::{self, *}, ValidationResult, }; @@ -30,26 +32,26 @@ pub fn validate_byron_tx( prot_magic: &u32, ) -> ValidationResult { let tx: &Tx = &mtxp.transaction; - let size: u64 = get_tx_size(tx)?; + let size: &u64 = &get_tx_size(tx)?; check_ins_not_empty(tx)?; check_outs_not_empty(tx)?; check_ins_in_utxos(tx, utxos)?; check_outs_have_lovelace(tx)?; - check_fees(tx, &size, utxos, prot_pps)?; - check_size(&size, prot_pps)?; + check_fees(tx, size, utxos, prot_pps)?; + check_size(size, prot_pps)?; check_witnesses(mtxp, utxos, prot_magic) } fn check_ins_not_empty(tx: &Tx) -> ValidationResult { if tx.inputs.clone().to_vec().is_empty() { - return Err(ValidationError::TxInsEmpty); + return Err(Byron(TxInsEmpty)); } Ok(()) } fn check_outs_not_empty(tx: &Tx) -> ValidationResult { if tx.outputs.clone().to_vec().is_empty() { - return Err(ValidationError::TxOutsEmpty); + return Err(Byron(TxOutsEmpty)); } Ok(()) } @@ -57,7 +59,7 @@ fn check_outs_not_empty(tx: &Tx) -> ValidationResult { fn check_ins_in_utxos(tx: &Tx, utxos: &UTxOs) -> ValidationResult { for input in tx.inputs.iter() { if !(utxos.contains_key(&MultiEraInput::from_byron(input))) { - return Err(ValidationError::InputMissingInUTxO); + return Err(Byron(InputNotInUTxO)); } } Ok(()) @@ -66,7 +68,7 @@ fn check_ins_in_utxos(tx: &Tx, utxos: &UTxOs) -> ValidationResult { fn check_outs_have_lovelace(tx: &Tx) -> ValidationResult { for output in tx.outputs.iter() { if output.amount == 0 { - return Err(ValidationError::OutputWithoutLovelace); + return Err(Byron(OutputWithoutLovelace)); } } Ok(()) @@ -84,7 +86,7 @@ fn check_fees(tx: &Tx, size: &u64, utxos: &UTxOs, prot_pps: &ByronProtParams) -> .and_then(MultiEraOutput::as_byron) { Some(byron_utxo) => inputs_balance += byron_utxo.amount, - None => return Err(ValidationError::UnableToComputeFees), + None => return Err(Byron(UnableToComputeFees)), } } if only_redeem_utxos { @@ -95,9 +97,9 @@ fn check_fees(tx: &Tx, size: &u64, utxos: &UTxOs, prot_pps: &ByronProtParams) -> outputs_balance += output.amount } let total_balance: u64 = inputs_balance - outputs_balance; - let min_fees: u64 = prot_pps.min_fees_const + prot_pps.min_fees_factor * size; + let min_fees: u64 = prot_pps.fee_policy.summand + prot_pps.fee_policy.multiplier * size; if total_balance < min_fees { - Err(ValidationError::FeesBelowMin) + Err(Byron(FeesBelowMin)) } else { Ok(()) } @@ -119,7 +121,7 @@ fn is_redeem_utxo(input: &TxIn, utxos: &UTxOs) -> bool { fn check_size(size: &u64, prot_pps: &ByronProtParams) -> ValidationResult { if *size > prot_pps.max_tx_size { - return Err(ValidationError::MaxTxSizeExceeded); + return Err(Byron(MaxTxSizeExceeded)); } Ok(()) } @@ -128,7 +130,7 @@ fn get_tx_size(tx: &Tx) -> Result { let mut buff: Vec = Vec::new(); match encode(tx, &mut buff) { Ok(()) => Ok(buff.len() as u64), - Err(_) => Err(ValidationError::UnknownTxSize), + Err(_) => Err(Byron(UnknownTxSize)), } } @@ -149,7 +151,7 @@ fn check_witnesses(mtxp: &MintedTxPayload, utxos: &UTxOs, prot_magic: &u32) -> V let data_to_verify: Vec = get_data_to_verify(sign, prot_magic, &tx_hash)?; let signature: Signature = get_signature(sign); if !public_key.verify(data_to_verify, &signature) { - return Err(ValidationError::WrongSignature); + return Err(Byron(WrongSignature)); } } Ok(()) @@ -165,7 +167,7 @@ fn tag_witnesses(wits: &[Twit]) -> Result, Valid Twit::RedeemWitness(CborWrap((pk, sig))) => { res.push((pk, TaggedSignature::RedeemWitness(sig))); } - _ => return Err(ValidationError::UnableToProcessWitnesses), + _ => return Err(Byron(UnableToProcessWitness)), } } Ok(res) @@ -175,9 +177,9 @@ fn find_tx_out<'a>(input: &'a TxIn, utxos: &'a UTxOs) -> Result<&'a TxOut, Valid let key: MultiEraInput = MultiEraInput::Byron(Box::new(Cow::Borrowed(input))); utxos .get(&key) - .ok_or(ValidationError::InputMissingInUTxO)? + .ok_or(Byron(InputNotInUTxO))? .as_byron() - .ok_or(ValidationError::InputMissingInUTxO) + .ok_or(Byron(InputNotInUTxO)) } fn find_raw_witness<'a>( @@ -187,7 +189,7 @@ fn find_raw_witness<'a>( let address: ByronAddress = mk_byron_address(&tx_out.address); let addr_payload: AddressPayload = address .decode() - .map_err(|_| ValidationError::UnableToProcessWitnesses)?; + .map_err(|_| Byron(UnableToProcessWitness))?; let root: AddressId = addr_payload.root; let attr: AddrAttrs = addr_payload.attributes; let addr_type: AddrType = addr_payload.addrtype; @@ -195,11 +197,11 @@ fn find_raw_witness<'a>( if redeems(pub_key, sign, &root, &attr, &addr_type) { match addr_type { AddrType::PubKey | AddrType::Redeem => return Ok((pub_key, sign)), - _ => return Err(ValidationError::UnableToProcessWitnesses), + _ => return Err(Byron(UnableToProcessWitness)), } } } - Err(ValidationError::MissingWitness) + Err(Byron(MissingWitness)) } fn mk_byron_address(addr: &Address) -> ByronAddress { @@ -250,17 +252,17 @@ fn get_data_to_verify( match sign { TaggedSignature::PkWitness(_) => { enc.encode(SigningTag::Tx as u64) - .map_err(|_| ValidationError::UnableToProcessWitnesses)?; + .map_err(|_| Byron(UnableToProcessWitness))?; } TaggedSignature::RedeemWitness(_) => { enc.encode(SigningTag::RedeemTx as u64) - .map_err(|_| ValidationError::UnableToProcessWitnesses)?; + .map_err(|_| Byron(UnableToProcessWitness))?; } } enc.encode(prot_magic) - .map_err(|_| ValidationError::UnableToProcessWitnesses)?; + .map_err(|_| Byron(UnableToProcessWitness))?; enc.encode(tx_hash) - .map_err(|_| ValidationError::UnableToProcessWitnesses)?; + .map_err(|_| Byron(UnableToProcessWitness))?; Ok(enc.into_writer().clone()) } diff --git a/pallas-applying/src/lib.rs b/pallas-applying/src/lib.rs index 78f48b3..82f9db8 100644 --- a/pallas-applying/src/lib.rs +++ b/pallas-applying/src/lib.rs @@ -1,24 +1,41 @@ //! Logic for validating and applying new blocks and txs to the chain state pub mod byron; +pub mod shelley_ma; pub mod types; use byron::validate_byron_tx; +use pallas_traverse::{Era, MultiEraTx}; +use shelley_ma::validate_shelley_ma_tx; -use pallas_traverse::{MultiEraTx, MultiEraTx::Byron as ByronTxPayload}; - -pub use types::{Environment, MultiEraProtParams, UTxOs, ValidationResult}; +pub use types::{ + Environment, MultiEraProtParams, UTxOs, ValidationError::TxAndProtParamsDiffer, + ValidationResult, +}; pub fn validate(metx: &MultiEraTx, utxos: &UTxOs, env: &Environment) -> ValidationResult { - match (metx, env) { - ( - ByronTxPayload(mtxp), - Environment { - prot_params: MultiEraProtParams::Byron(bpp), - prot_magic, + match env { + Environment { + prot_params: MultiEraProtParams::Byron(bpp), + prot_magic, + .. + } => match metx { + MultiEraTx::Byron(mtxp) => validate_byron_tx(mtxp, utxos, bpp, prot_magic), + _ => Err(TxAndProtParamsDiffer), + }, + Environment { + prot_params: MultiEraProtParams::Shelley(spp), + block_slot, + network_id, + .. + } => match metx.era() { + Era::Shelley | Era::Allegra | Era::Mary => match metx.as_alonzo() { + Some(mtx) => { + validate_shelley_ma_tx(mtx, utxos, spp, block_slot, network_id, &metx.era()) + } + None => Err(TxAndProtParamsDiffer), }, - ) => validate_byron_tx(mtxp, utxos, bpp, prot_magic), - // TODO: implement the rest of the eras. - _ => Ok(()), + _ => Err(TxAndProtParamsDiffer), + }, } } diff --git a/pallas-applying/src/shelley_ma.rs b/pallas-applying/src/shelley_ma.rs new file mode 100644 index 0000000..b05930a --- /dev/null +++ b/pallas-applying/src/shelley_ma.rs @@ -0,0 +1,524 @@ +//! Utilities required for Shelley-era transaction validation. + +use crate::types::{ + FeePolicy, + ShelleyMAError::*, + ShelleyProtParams, UTxOs, + ValidationError::{self, *}, + ValidationResult, +}; +use pallas_addresses::{Address, PaymentKeyHash, ScriptHash, ShelleyAddress, ShelleyPaymentPart}; +use pallas_codec::{ + minicbor::encode, + utils::{Bytes, KeepRaw, KeyValuePairs}, +}; +use pallas_crypto::key::ed25519::{PublicKey, Signature}; +use pallas_primitives::{ + alonzo::{ + AssetName, AuxiliaryData, Coin, MintedTx, MintedWitnessSet, Multiasset, NativeScript, + PolicyId, TransactionBody, TransactionOutput, VKeyWitness, Value, + }, + byron::TxOut, +}; +use pallas_traverse::{ComputeHash, Era, MultiEraInput, MultiEraOutput}; +use std::collections::HashMap; + +// TODO: implement each of the validation rules. +pub fn validate_shelley_ma_tx( + mtx: &MintedTx, + utxos: &UTxOs, + prot_pps: &ShelleyProtParams, + block_slot: &u64, + network_id: &u8, + era: &Era, +) -> ValidationResult { + let tx_body: &TransactionBody = &mtx.transaction_body; + let tx_wits: &MintedWitnessSet = &mtx.transaction_witness_set; + let size: &u64 = &get_tx_size(tx_body)?; + let auxiliary_data_hash: &Option = &tx_body.auxiliary_data_hash; + let auxiliary_data: &Option<&[u8]> = &extract_auxiliary_data(mtx); + let minted_value: &Option> = &tx_body.mint; + let native_script_wits: &Option> = &mtx.transaction_witness_set.native_script; + check_ins_not_empty(tx_body)?; + check_ins_in_utxos(tx_body, utxos)?; + check_ttl(tx_body, block_slot)?; + check_size(size, prot_pps)?; + check_min_lovelace(tx_body, prot_pps, era)?; + check_preservation_of_value(tx_body, utxos, era)?; + check_fees(tx_body, size, &prot_pps.fee_policy)?; + check_network_id(tx_body, network_id)?; + check_metadata(auxiliary_data_hash, auxiliary_data)?; + check_witnesses(tx_body, utxos, tx_wits)?; + check_minting(minted_value, native_script_wits) +} + +fn get_tx_size(tx_body: &TransactionBody) -> Result { + let mut buff: Vec = Vec::new(); + match encode(tx_body, &mut buff) { + Ok(()) => Ok(buff.len() as u64), + Err(_) => Err(Shelley(UnknownTxSize)), + } +} + +fn extract_auxiliary_data<'a>(mtx: &'a MintedTx) -> Option<&'a [u8]> { + Option::>::from((mtx.auxiliary_data).clone()) + .as_ref() + .map(KeepRaw::raw_cbor) +} + +fn check_ins_not_empty(tx_body: &TransactionBody) -> ValidationResult { + if tx_body.inputs.is_empty() { + return Err(Shelley(TxInsEmpty)); + } + Ok(()) +} + +fn check_ins_in_utxos(tx_body: &TransactionBody, utxos: &UTxOs) -> ValidationResult { + for input in tx_body.inputs.iter() { + if !(utxos.contains_key(&MultiEraInput::from_alonzo_compatible(input))) { + return Err(Shelley(InputNotInUTxO)); + } + } + Ok(()) +} + +fn check_ttl(tx_body: &TransactionBody, block_slot: &u64) -> ValidationResult { + match tx_body.ttl { + Some(ttl) => { + if ttl < *block_slot { + Err(Shelley(TTLExceeded)) + } else { + Ok(()) + } + } + None => Err(Shelley(AlonzoCompNotShelley)), + } +} + +fn check_size(size: &u64, prot_pps: &ShelleyProtParams) -> ValidationResult { + if *size > prot_pps.max_tx_size { + return Err(Shelley(MaxTxSizeExceeded)); + } + Ok(()) +} + +fn check_min_lovelace( + tx_body: &TransactionBody, + prot_pps: &ShelleyProtParams, + era: &Era, +) -> ValidationResult { + for TransactionOutput { amount, .. } in &tx_body.outputs { + match (era, amount) { + (Era::Shelley, Value::Coin(lovelace)) + | (Era::Allegra, Value::Coin(lovelace)) + | (Era::Mary, Value::Multiasset(lovelace, _)) => { + if *lovelace < prot_pps.min_lovelace { + return Err(Shelley(MinLovelaceUnreached)); + } + } + _ => return Err(Shelley(ValueNotShelley)), + } + } + Ok(()) +} + +fn check_preservation_of_value( + tx_body: &TransactionBody, + utxos: &UTxOs, + era: &Era, +) -> ValidationResult { + let input: Value = get_consumed(tx_body, utxos, era)?; + let produced: Value = get_produced(tx_body, era)?; + let output: Value = add_values(&produced, &Value::Coin(tx_body.fee))?; + if let Some(m) = &tx_body.mint { + add_minted_value(&output, m)?; + } + if !values_are_equal(&input, &output) { + return Err(Shelley(PreservationOfValue)); + } + Ok(()) +} + +fn get_consumed( + tx_body: &TransactionBody, + utxos: &UTxOs, + era: &Era, +) -> Result { + let mut res: Value = empty_value(); + for input in tx_body.inputs.iter() { + let utxo_value: &MultiEraOutput = utxos + .get(&MultiEraInput::from_alonzo_compatible(input)) + .ok_or(Shelley(InputNotInUTxO))?; + match MultiEraOutput::as_alonzo(utxo_value) { + Some(TransactionOutput { amount, .. }) => match (amount, era) { + (Value::Coin(..), _) => res = add_values(&res, amount)?, + (Value::Multiasset(..), Era::Shelley) => return Err(Shelley(ValueNotShelley)), + _ => res = add_values(&res, amount)?, + }, + None => match MultiEraOutput::as_byron(utxo_value) { + Some(TxOut { amount, .. }) => res = add_values(&res, &Value::Coin(*amount))?, + _ => return Err(Shelley(InputNotInUTxO)), + }, + } + } + Ok(res) +} + +fn get_produced(tx_body: &TransactionBody, era: &Era) -> Result { + let mut res: Value = empty_value(); + for TransactionOutput { amount, .. } in tx_body.outputs.iter() { + match (amount, era) { + (Value::Coin(..), _) => res = add_values(&res, amount)?, + (Value::Multiasset(..), Era::Shelley) => return Err(Shelley(WrongEraOutput)), + _ => res = add_values(&res, amount)?, + } + } + Ok(res) +} + +fn empty_value() -> Value { + Value::Multiasset(0, Multiasset::::from(Vec::new())) +} + +fn add_values(first: &Value, second: &Value) -> Result { + match (first, second) { + (Value::Coin(f), Value::Coin(s)) => Ok(Value::Coin(f + s)), + (Value::Multiasset(f, fma), Value::Coin(s)) => Ok(Value::Multiasset(f + s, fma.clone())), + (Value::Coin(f), Value::Multiasset(s, sma)) => Ok(Value::Multiasset(f + s, sma.clone())), + (Value::Multiasset(f, fma), Value::Multiasset(s, sma)) => Ok(Value::Multiasset( + f + s, + coerce_to_coin(&add_multiasset_values( + &coerce_to_i64(fma), + &coerce_to_i64(sma), + ))?, + )), + } +} + +fn add_minted_value( + base_value: &Value, + minted_value: &Multiasset, +) -> Result { + match base_value { + Value::Coin(n) => Ok(Value::Multiasset(*n, coerce_to_coin(minted_value)?)), + Value::Multiasset(n, mary_base_value) => Ok(Value::Multiasset( + *n, + coerce_to_coin(&add_multiasset_values( + &coerce_to_i64(mary_base_value), + minted_value, + ))?, + )), + } +} + +fn coerce_to_i64(value: &Multiasset) -> Multiasset { + let mut res: Vec<(PolicyId, KeyValuePairs)> = Vec::new(); + for (policy, assets) in value.clone().to_vec().iter() { + let mut aa: Vec<(AssetName, i64)> = Vec::new(); + for (asset_name, amount) in assets.clone().to_vec().iter() { + aa.push((asset_name.clone(), *amount as i64)); + } + res.push((*policy, KeyValuePairs::::from(aa))); + } + KeyValuePairs::>::from(res) +} + +fn coerce_to_coin(value: &Multiasset) -> Result, ValidationError> { + let mut res: Vec<(PolicyId, KeyValuePairs)> = Vec::new(); + for (policy, assets) in value.clone().to_vec().iter() { + let mut aa: Vec<(AssetName, Coin)> = Vec::new(); + for (asset_name, amount) in assets.clone().to_vec().iter() { + if *amount < 0 { + return Err(Shelley(NegativeValue)); + } + aa.push((asset_name.clone(), *amount as u64)); + } + res.push((*policy, KeyValuePairs::::from(aa))); + } + Ok(KeyValuePairs::>::from(res)) +} + +fn add_multiasset_values(first: &Multiasset, second: &Multiasset) -> Multiasset { + let mut res: HashMap> = HashMap::new(); + for (policy, new_assets) in first.iter() { + match res.get(policy) { + Some(old_assets) => res.insert(*policy, add_same_policy_assets(old_assets, new_assets)), + None => res.insert(*policy, add_same_policy_assets(&HashMap::new(), new_assets)), + }; + } + for (policy, new_assets) in second.iter() { + match res.get(policy) { + Some(old_assets) => res.insert(*policy, add_same_policy_assets(old_assets, new_assets)), + None => res.insert(*policy, add_same_policy_assets(&HashMap::new(), new_assets)), + }; + } + wrap_multiasset(res) +} + +fn add_same_policy_assets( + old_assets: &HashMap, + new_assets: &KeyValuePairs, +) -> HashMap { + let mut res: HashMap = old_assets.clone(); + for (asset_name, new_amount) in new_assets.iter() { + match res.get(asset_name) { + Some(old_amount) => res.insert(asset_name.clone(), old_amount + *new_amount), + None => res.insert(asset_name.clone(), *new_amount), + }; + } + res +} + +fn wrap_multiasset(input: HashMap>) -> Multiasset { + Multiasset::::from( + input + .into_iter() + .map(|(policy, assets)| { + ( + policy, + KeyValuePairs::::from( + assets.into_iter().collect::>(), + ), + ) + }) + .collect::)>>(), + ) +} + +fn values_are_equal(first: &Value, second: &Value) -> bool { + match (first, second) { + (Value::Coin(f), Value::Coin(s)) => f == s, + (Value::Multiasset(..), Value::Coin(..)) => false, + (Value::Coin(..), Value::Multiasset(..)) => false, + (Value::Multiasset(f, fma), Value::Multiasset(s, sma)) => { + if f != s { + false + } else { + for (fpolicy, fassets) in fma.iter() { + match find_policy(sma, fpolicy) { + Some(sassets) => { + for (fasset_name, famount) in fassets.iter() { + match find_assets(&sassets, fasset_name) { + Some(samount) => { + if *famount != samount { + return false; + } + } + None => return false, + }; + } + } + None => return false, + } + } + true + } + } + } +} + +fn find_policy( + mary_value: &Multiasset, + search_policy: &PolicyId, +) -> Option> { + for (policy, assets) in mary_value.clone().to_vec().iter() { + if policy == search_policy { + return Some(assets.clone()); + } + } + None +} + +fn find_assets(assets: &KeyValuePairs, asset_name: &AssetName) -> Option { + for (an, amount) in assets.clone().to_vec().iter() { + if an == asset_name { + return Some(*amount); + } + } + None +} + +fn check_fees(tx_body: &TransactionBody, size: &u64, fee_policy: &FeePolicy) -> ValidationResult { + if tx_body.fee < fee_policy.summand + fee_policy.multiplier * size { + return Err(Shelley(FeesBelowMin)); + } + Ok(()) +} + +fn check_network_id(tx_body: &TransactionBody, network_id: &u8) -> ValidationResult { + for output in tx_body.outputs.iter() { + let addr: ShelleyAddress = get_shelley_address(Vec::::from(output.address.clone()))?; + if addr.network().value() != *network_id { + return Err(Shelley(WrongNetworkID)); + } + } + Ok(()) +} + +fn get_shelley_address(address: Vec) -> Result { + match Address::from_bytes(&address) { + Ok(Address::Shelley(sa)) => Ok(sa), + Ok(_) => Err(Shelley(WrongEraOutput)), + Err(_) => Err(Shelley(AddressDecoding)), + } +} + +fn check_metadata( + auxiliary_data_hash: &Option, + auxiliary_data_cbor: &Option<&[u8]>, +) -> ValidationResult { + match (auxiliary_data_hash, auxiliary_data_cbor) { + (Some(metadata_hash), Some(metadata)) => { + if metadata_hash.as_slice() + == pallas_crypto::hash::Hasher::<256>::hash(metadata).as_ref() + { + Ok(()) + } else { + Err(Shelley(MetadataHash)) + } + } + (None, None) => Ok(()), + _ => Err(Shelley(MetadataHash)), + } +} + +fn check_witnesses( + tx_body: &TransactionBody, + utxos: &UTxOs, + tx_wits: &MintedWitnessSet, +) -> ValidationResult { + let wits: &mut Vec<(bool, VKeyWitness)> = &mut mk_vkwitness_check_list(&tx_wits.vkeywitness)?; + let tx_hash: &Vec = &Vec::from(tx_body.compute_hash().as_ref()); + for input in tx_body.inputs.iter() { + match utxos.get(&MultiEraInput::from_alonzo_compatible(input)) { + Some(multi_era_output) => { + if let Some(alonzo_comp_output) = MultiEraOutput::as_alonzo(multi_era_output) { + match get_payment_part(alonzo_comp_output)? { + ShelleyPaymentPart::Key(payment_key_hash) => { + check_verification_key_witness(&payment_key_hash, tx_hash, wits)? + } + ShelleyPaymentPart::Script(script_hash) => { + check_native_script_witness(&script_hash, &tx_wits.native_script)? + } + } + } + } + None => return Err(Shelley(InputNotInUTxO)), + } + } + check_remaining_verification_key_witnesses(wits, tx_hash) +} + +fn mk_vkwitness_check_list( + wits: &Option>, +) -> Result, ValidationError> { + Ok(wits + .clone() + .ok_or(Shelley(MissingVKWitness))? + .iter() + .map(|x| (false, x.clone())) + .collect::>()) +} + +fn get_payment_part(tx_out: &TransactionOutput) -> Result { + let addr: ShelleyAddress = get_shelley_address(Vec::::from(tx_out.address.clone()))?; + Ok(addr.payment().clone()) +} + +fn check_verification_key_witness( + payment_key_hash: &PaymentKeyHash, + data_to_verify: &Vec, + wits: &mut Vec<(bool, VKeyWitness)>, +) -> ValidationResult { + for (found, VKeyWitness { vkey, signature }) in wits { + if pallas_crypto::hash::Hasher::<224>::hash(vkey) == *payment_key_hash { + let mut public_key_source: [u8; PublicKey::SIZE] = [0; PublicKey::SIZE]; + public_key_source.copy_from_slice(vkey.as_slice()); + let public_key: PublicKey = From::<[u8; PublicKey::SIZE]>::from(public_key_source); + let mut signature_source: [u8; Signature::SIZE] = [0; Signature::SIZE]; + signature_source.copy_from_slice(signature.as_slice()); + let sig: Signature = From::<[u8; Signature::SIZE]>::from(signature_source); + if public_key.verify(data_to_verify, &sig) { + *found = true; + return Ok(()); + } else { + return Err(Shelley(WrongSignature)); + } + } + } + Err(Shelley(MissingVKWitness)) +} + +fn check_native_script_witness( + script_hash: &ScriptHash, + wits: &Option>, +) -> ValidationResult { + match wits { + Some(scripts) => { + let mut payload: Vec = vec![0u8]; + for script in scripts.iter() { + let _ = encode(script, &mut payload); + if pallas_crypto::hash::Hasher::<224>::hash(&payload) == *script_hash { + return Ok(()); + } + } + Err(Shelley(MissingScriptWitness)) + } + None => Err(Shelley(MissingScriptWitness)), + } +} + +fn check_remaining_verification_key_witnesses( + wits: &mut Vec<(bool, VKeyWitness)>, + data_to_verify: &Vec, +) -> ValidationResult { + for (covered, VKeyWitness { vkey, signature }) in wits { + if !*covered { + let mut public_key_source: [u8; PublicKey::SIZE] = [0; PublicKey::SIZE]; + public_key_source.copy_from_slice(vkey.as_slice()); + let public_key: PublicKey = From::<[u8; PublicKey::SIZE]>::from(public_key_source); + let mut signature_source: [u8; Signature::SIZE] = [0; Signature::SIZE]; + signature_source.copy_from_slice(signature.as_slice()); + let sig: Signature = From::<[u8; Signature::SIZE]>::from(signature_source); + if !public_key.verify(data_to_verify, &sig) { + return Err(Shelley(WrongSignature)); + } + } + } + Ok(()) +} + +fn check_minting( + values: &Option>, + scripts: &Option>, +) -> ValidationResult { + match (values, scripts) { + (None, _) => Ok(()), + (Some(_), None) => Err(Shelley(MintingLacksPolicy)), + (Some(minted_value), Some(native_script_wits)) => { + for (policy, _) in minted_value.iter() { + if check_policy(policy, native_script_wits) { + return Ok(()); + } + } + Ok(()) + } + } +} + +fn check_policy(policy: &PolicyId, native_script_wits: &[NativeScript]) -> bool { + for script in native_script_wits.iter() { + let hashed_script: PolicyId = compute_script_hash(script); + if *policy == hashed_script { + return true; + } + } + false +} + +fn compute_script_hash(script: &NativeScript) -> PolicyId { + let mut payload = Vec::new(); + let _ = encode(script, &mut payload); + payload.insert(0, 0); + pallas_crypto::hash::Hasher::<224>::hash(&payload) +} diff --git a/pallas-applying/src/types.rs b/pallas-applying/src/types.rs index a6b9712..92ef2ed 100644 --- a/pallas-applying/src/types.rs +++ b/pallas-applying/src/types.rs @@ -8,22 +8,37 @@ pub type UTxOs<'b> = HashMap, MultiEraOutput<'b>>; #[derive(Debug, Clone)] pub struct ByronProtParams { - pub min_fees_const: u64, - pub min_fees_factor: u64, + pub fee_policy: FeePolicy, pub max_tx_size: u64, } +#[derive(Debug, Clone)] +pub struct ShelleyProtParams { + pub fee_policy: FeePolicy, + pub max_tx_size: u64, + pub min_lovelace: u64, +} + +#[derive(Debug, Clone)] +pub struct FeePolicy { + pub summand: u64, + pub multiplier: u64, +} + // TODO: add variants for the other eras. #[derive(Debug)] #[non_exhaustive] pub enum MultiEraProtParams { Byron(ByronProtParams), + Shelley(ShelleyProtParams), } #[derive(Debug)] pub struct Environment { pub prot_params: MultiEraProtParams, pub prot_magic: u32, + pub block_slot: u64, + pub network_id: u8, } #[non_exhaustive] @@ -35,17 +50,49 @@ pub enum SigningTag { #[derive(Debug)] #[non_exhaustive] pub enum ValidationError { - InputMissingInUTxO, + TxAndProtParamsDiffer, + Byron(ByronError), + Shelley(ShelleyMAError), +} + +#[derive(Debug)] +#[non_exhaustive] +pub enum ByronError { TxInsEmpty, TxOutsEmpty, + InputNotInUTxO, OutputWithoutLovelace, UnknownTxSize, UnableToComputeFees, FeesBelowMin, MaxTxSizeExceeded, - UnableToProcessWitnesses, + UnableToProcessWitness, MissingWitness, WrongSignature, } +#[derive(Debug)] +#[non_exhaustive] +pub enum ShelleyMAError { + TxInsEmpty, + InputNotInUTxO, + TTLExceeded, + AlonzoCompNotShelley, + UnknownTxSize, + MaxTxSizeExceeded, + ValueNotShelley, + MinLovelaceUnreached, + PreservationOfValue, + NegativeValue, + FeesBelowMin, + WrongEraOutput, + AddressDecoding, + WrongNetworkID, + MetadataHash, + MissingVKWitness, + MissingScriptWitness, + WrongSignature, + MintingLacksPolicy, +} + pub type ValidationResult = Result<(), ValidationError>; diff --git a/pallas-applying/tests/byron.rs b/pallas-applying/tests/byron.rs index 8e0f518..fa48a43 100644 --- a/pallas-applying/tests/byron.rs +++ b/pallas-applying/tests/byron.rs @@ -1,8 +1,11 @@ use std::{borrow::Cow, vec::Vec}; use pallas_applying::{ - types::{ByronProtParams, Environment, MultiEraProtParams, ValidationError}, - validate, UTxOs, ValidationResult, + types::{ + ByronError::*, ByronProtParams, Environment, FeePolicy, MultiEraProtParams, + ValidationError::*, + }, + validate, UTxOs, }; use pallas_codec::{ minicbor::{ @@ -10,67 +13,11 @@ use pallas_codec::{ decode::{Decode, Decoder}, encode, }, - utils::{CborWrap, KeepRaw, MaybeIndefArray, TagWrap}, + utils::{CborWrap, MaybeIndefArray, TagWrap}, }; use pallas_primitives::byron::{Address, MintedTxPayload, Twit, Tx, TxIn, TxOut, Witnesses}; use pallas_traverse::{MultiEraInput, MultiEraOutput, MultiEraTx}; -// Helper functions. -fn add_to_utxo(utxos: &mut UTxOs, tx_in: TxIn, tx_out: TxOut) { - let multi_era_in: MultiEraInput = MultiEraInput::Byron(Box::new(Cow::Owned(tx_in))); - let multi_era_out: MultiEraOutput = MultiEraOutput::Byron(Box::new(Cow::Owned(tx_out))); - utxos.insert(multi_era_in, multi_era_out); -} - -// pallas_applying::validate takes a MultiEraTx, not a Tx and a Witnesses. To be -// able to build a MultiEraTx from a Tx and a Witnesses, we need to encode each -// of them and then decode them into KeepRaw and KeepRaw values, -// respectively, to be able to make the MultiEraTx value. -fn mk_byron_tx_and_validate( - tx: &Tx, - wits: &Witnesses, - utxos: &UTxOs, - env: &Environment, -) -> ValidationResult { - let mut tx_buf: Vec = Vec::new(); - - match encode(tx, &mut tx_buf) { - Ok(_) => (), - Err(err) => panic!("Unable to encode Tx ({:?}).", err), - }; - - let kptx: KeepRaw = match Decode::decode(&mut Decoder::new(tx_buf.as_slice()), &mut ()) { - Ok(kp) => kp, - Err(err) => panic!("Unable to decode Tx ({:?}).", err), - }; - - let mut wit_buf: Vec = Vec::new(); - - match encode(wits, &mut wit_buf) { - Ok(_) => (), - Err(err) => panic!("Unable to encode Witnesses ({:?}).", err), - }; - - let kpwit: KeepRaw = - match Decode::decode(&mut Decoder::new(wit_buf.as_slice()), &mut ()) { - Ok(kp) => kp, - Err(err) => panic!("Unable to decode Witnesses ({:?}).", err), - }; - - let mtxp: MintedTxPayload = MintedTxPayload { - transaction: kptx, - witness: kpwit, - }; - - let metx: MultiEraTx = MultiEraTx::from_byron(&mtxp); - - validate(&metx, utxos, env) -} - -fn new_utxos<'a>() -> UTxOs<'a> { - UTxOs::new() -} - #[cfg(test)] mod byron_tests { use super::*; @@ -79,60 +26,66 @@ mod byron_tests { hex::decode(input).unwrap() } - fn mainnet_tx_from_bytes_cbor(tx_cbor: &[u8]) -> MintedTxPayload<'_> { - pallas_codec::minicbor::decode::(tx_cbor).unwrap() + fn tx_from_cbor<'a>(tx_cbor: &'a Vec) -> MintedTxPayload<'a> { + pallas_codec::minicbor::decode::(&tx_cbor[..]).unwrap() } // Careful: this function assumes tx has exactly one input. fn mk_utxo_for_single_input_tx<'a>(tx: &Tx, address_payload: String, amount: u64) -> UTxOs<'a> { let mut tx_ins: Vec = tx.inputs.clone().to_vec(); - assert_eq!(tx_ins.len(), 1, "Unexpected number of inputs."); + assert_eq!(tx_ins.len(), 1, "Unexpected number of inputs"); let tx_in: TxIn = tx_ins.pop().unwrap(); let input_tx_out_addr: Address = match hex::decode(address_payload) { Ok(addr_bytes) => Address { payload: TagWrap(ByteVec::from(addr_bytes)), crc: 3430631884, }, - _ => panic!("Unable to decode input address."), + _ => panic!("Unable to decode input address"), }; let tx_out: TxOut = TxOut { address: input_tx_out_addr, amount, }; - let mut utxos: UTxOs = new_utxos(); + let mut utxos: UTxOs = UTxOs::new(); add_to_utxo(&mut utxos, tx_in, tx_out); utxos } #[test] + // Transaction hash: a9e4413a5fb61a7a43c7df006ffcaaf3f2ffc9541f54757023968c5a8f8294fd fn successful_mainnet_tx_with_genesis_utxos() { let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron2.tx")); - let mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let mtxp: MintedTxPayload = tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_byron(&mtxp); let utxos: UTxOs = mk_utxo_for_single_input_tx( &mtxp.transaction, String::from(include_str!("../../test_data/byron2.address")), - // The number of lovelace in this input is irrelevant, since no fees have to be paid - // for this transaction. - 1, + 19999000000, ); let env: Environment = Environment { prot_params: MultiEraProtParams::Byron(ByronProtParams { - min_fees_const: 155381, - min_fees_factor: 44, + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, max_tx_size: 4096, }), prot_magic: 764824073, + block_slot: 6341, + network_id: 1, }; - match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + match validate(&metx, &utxos, &env) { Ok(()) => (), - Err(err) => panic!("Unexpected error ({:?}).", err), + Err(err) => panic!("Unexpected error ({:?})", err), } } #[test] + // Transaction hash: a06e5a0150e09f8983be2deafab9e04afc60d92e7110999eb672c903343f1e26 fn successful_mainnet_tx() { let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); - let mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let mtxp: MintedTxPayload = tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_byron(&mtxp); let utxos: UTxOs = mk_utxo_for_single_input_tx( &mtxp.transaction, String::from(include_str!("../../test_data/byron1.address")), @@ -140,15 +93,19 @@ mod byron_tests { ); let env: Environment = Environment { prot_params: MultiEraProtParams::Byron(ByronProtParams { - min_fees_const: 155381, - min_fees_factor: 44, + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, max_tx_size: 4096, }), prot_magic: 764824073, + block_slot: 3241381, + network_id: 1, }; - match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + match validate(&metx, &utxos, &env) { Ok(()) => (), - Err(err) => panic!("Unexpected error ({:?}).", err), + Err(err) => panic!("Unexpected error ({:?})", err), } } @@ -156,7 +113,7 @@ mod byron_tests { // Identical to successful_mainnet_tx, except that all inputs are removed. fn empty_ins() { let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); - let mut mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let mut mtxp: MintedTxPayload = tx_from_cbor(&cbor_bytes); let utxos: UTxOs = mk_utxo_for_single_input_tx( &mtxp.transaction, String::from(include_str!("../../test_data/byron1.address")), @@ -168,22 +125,27 @@ mod byron_tests { let mut tx_buf: Vec = Vec::new(); match encode(tx, &mut tx_buf) { Ok(_) => (), - Err(err) => panic!("Unable to encode Tx ({:?}).", err), + Err(err) => panic!("Unable to encode Tx ({:?})", err), }; - mtxp.transaction = Decode::decode(&mut Decoder::new(tx_buf.as_slice()), &mut ()).unwrap(); + mtxp.transaction = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let metx: MultiEraTx = MultiEraTx::from_byron(&mtxp); let env: Environment = Environment { prot_params: MultiEraProtParams::Byron(ByronProtParams { - min_fees_const: 155381, - min_fees_factor: 44, + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, max_tx_size: 4096, }), prot_magic: 764824073, + block_slot: 3241381, + network_id: 1, }; - match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { - Ok(()) => panic!("Inputs set should not be empty."), + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Inputs set should not be empty"), Err(err) => match err { - ValidationError::TxInsEmpty => (), - _ => panic!("Unexpected error ({:?}).", err), + Byron(TxInsEmpty) => (), + _ => panic!("Unexpected error ({:?})", err), }, } } @@ -192,34 +154,39 @@ mod byron_tests { // Identical to successful_mainnet_tx, except that all outputs are removed. fn empty_outs() { let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); - let mut mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); - let utxos: UTxOs = mk_utxo_for_single_input_tx( - &mtxp.transaction, - String::from(include_str!("../../test_data/byron1.address")), - 19999000000, - ); + let mut mtxp: MintedTxPayload = tx_from_cbor(&cbor_bytes); // Clear the set of outputs in the transaction. let mut tx: Tx = (*mtxp.transaction).clone(); tx.outputs = MaybeIndefArray::Def(Vec::new()); let mut tx_buf: Vec = Vec::new(); match encode(tx, &mut tx_buf) { Ok(_) => (), - Err(err) => panic!("Unable to encode Tx ({:?}).", err), + Err(err) => panic!("Unable to encode Tx ({:?})", err), }; - mtxp.transaction = Decode::decode(&mut Decoder::new(tx_buf.as_slice()), &mut ()).unwrap(); + mtxp.transaction = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let metx: MultiEraTx = MultiEraTx::from_byron(&mtxp); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtxp.transaction, + String::from(include_str!("../../test_data/byron1.address")), + 19999000000, + ); let env: Environment = Environment { prot_params: MultiEraProtParams::Byron(ByronProtParams { - min_fees_const: 155381, - min_fees_factor: 44, + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, max_tx_size: 4096, }), prot_magic: 764824073, + block_slot: 3241381, + network_id: 1, }; - match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { - Ok(()) => panic!("Outputs set should not be empty."), + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Outputs set should not be empty"), Err(err) => match err { - ValidationError::TxOutsEmpty => (), - _ => panic!("Unexpected error ({:?}).", err), + Byron(TxOutsEmpty) => (), + _ => panic!("Unexpected error ({:?})", err), }, } } @@ -228,21 +195,26 @@ mod byron_tests { // The transaction is valid, but the UTxO set is empty. fn unfound_utxo() { let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); - let mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let mtxp: MintedTxPayload = tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_byron(&mtxp); let utxos: UTxOs = UTxOs::new(); let env: Environment = Environment { prot_params: MultiEraProtParams::Byron(ByronProtParams { - min_fees_const: 155381, - min_fees_factor: 44, + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, max_tx_size: 4096, }), prot_magic: 764824073, + block_slot: 3241381, + network_id: 1, }; - match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { - Ok(()) => panic!("All inputs must be within the UTxO set."), + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "All inputs must be within the UTxO set"), Err(err) => match err { - ValidationError::InputMissingInUTxO => (), - _ => panic!("Unexpected error ({:?}).", err), + Byron(InputNotInUTxO) => (), + _ => panic!("Unexpected error ({:?})", err), }, } } @@ -251,12 +223,7 @@ mod byron_tests { // All lovelace in one of the outputs was removed. fn output_without_lovelace() { let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); - let mut mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); - let utxos: UTxOs = mk_utxo_for_single_input_tx( - &mtxp.transaction, - String::from(include_str!("../../test_data/byron1.address")), - 19999000000, - ); + let mut mtxp: MintedTxPayload = tx_from_cbor(&cbor_bytes); // Remove lovelace from output. let mut tx: Tx = (*mtxp.transaction).clone(); let altered_tx_out: TxOut = TxOut { @@ -269,22 +236,32 @@ mod byron_tests { let mut tx_buf: Vec = Vec::new(); match encode(tx, &mut tx_buf) { Ok(_) => (), - Err(err) => panic!("Unable to encode Tx ({:?}).", err), + Err(err) => panic!("Unable to encode Tx ({:?})", err), }; - mtxp.transaction = Decode::decode(&mut Decoder::new(tx_buf.as_slice()), &mut ()).unwrap(); + mtxp.transaction = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let metx: MultiEraTx = MultiEraTx::from_byron(&mtxp); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtxp.transaction, + String::from(include_str!("../../test_data/byron1.address")), + 19999000000, + ); let env: Environment = Environment { prot_params: MultiEraProtParams::Byron(ByronProtParams { - min_fees_const: 155381, - min_fees_factor: 44, + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, max_tx_size: 4096, }), prot_magic: 764824073, + block_slot: 3241381, + network_id: 1, }; - match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { - Ok(()) => panic!("All outputs must contain lovelace."), + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "All outputs must contain lovelace"), Err(err) => match err { - ValidationError::OutputWithoutLovelace => (), - _ => panic!("Unexpected error ({:?}).", err), + Byron(OutputWithoutLovelace) => (), + _ => panic!("Unexpected error ({:?})", err), }, } } @@ -293,7 +270,8 @@ mod byron_tests { // Expected fees are increased by increasing the protocol parameters. fn not_enough_fees() { let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); - let mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let mtxp: MintedTxPayload = tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_byron(&mtxp); let utxos: UTxOs = mk_utxo_for_single_input_tx( &mtxp.transaction, String::from(include_str!("../../test_data/byron1.address")), @@ -301,17 +279,21 @@ mod byron_tests { ); let env: Environment = Environment { prot_params: MultiEraProtParams::Byron(ByronProtParams { - min_fees_const: 1000, - min_fees_factor: 1000, + fee_policy: FeePolicy { + summand: 1000, + multiplier: 1000, + }, max_tx_size: 4096, }), prot_magic: 764824073, + block_slot: 3241381, + network_id: 1, }; - match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { - Ok(()) => panic!("Fees should not be below minimum."), + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Fees should not be below minimum"), Err(err) => match err { - ValidationError::FeesBelowMin => (), - _ => panic!("Unexpected error ({:?}).", err), + Byron(FeesBelowMin) => (), + _ => panic!("Unexpected error ({:?})", err), }, } } @@ -320,7 +302,8 @@ mod byron_tests { // Tx size limit set by protocol parameters is established at 0. fn tx_size_exceeds_max() { let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); - let mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let mtxp: MintedTxPayload = tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_byron(&mtxp); let utxos: UTxOs = mk_utxo_for_single_input_tx( &mtxp.transaction, String::from(include_str!("../../test_data/byron1.address")), @@ -328,17 +311,21 @@ mod byron_tests { ); let env: Environment = Environment { prot_params: MultiEraProtParams::Byron(ByronProtParams { - min_fees_const: 155381, - min_fees_factor: 44, + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, max_tx_size: 0, }), prot_magic: 764824073, + block_slot: 3241381, + network_id: 1, }; - match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { - Ok(()) => panic!("Transaction size cannot exceed protocol limit."), + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Transaction size cannot exceed protocol limit"), Err(err) => match err { - ValidationError::MaxTxSizeExceeded => (), - _ => panic!("Unexpected error ({:?}).", err), + Byron(MaxTxSizeExceeded) => (), + _ => panic!("Unexpected error ({:?})", err), }, } } @@ -347,33 +334,38 @@ mod byron_tests { // The input to the transaction does not have a corresponding witness. fn missing_witness() { let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); - let mut mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); - let utxos: UTxOs = mk_utxo_for_single_input_tx( - &mtxp.transaction, - String::from(include_str!("../../test_data/byron1.address")), - 19999000000, - ); + let mut mtxp: MintedTxPayload = tx_from_cbor(&cbor_bytes); // Remove witness let new_witnesses: Witnesses = MaybeIndefArray::Def(Vec::new()); let mut tx_buf: Vec = Vec::new(); match encode(new_witnesses, &mut tx_buf) { Ok(_) => (), - Err(err) => panic!("Unable to encode Tx ({:?}).", err), + Err(err) => panic!("Unable to encode Tx ({:?})", err), }; - mtxp.witness = Decode::decode(&mut Decoder::new(tx_buf.as_slice()), &mut ()).unwrap(); + mtxp.witness = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let metx: MultiEraTx = MultiEraTx::from_byron(&mtxp); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtxp.transaction, + String::from(include_str!("../../test_data/byron1.address")), + 19999000000, + ); let env: Environment = Environment { prot_params: MultiEraProtParams::Byron(ByronProtParams { - min_fees_const: 155381, - min_fees_factor: 44, + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, max_tx_size: 4096, }), prot_magic: 764824073, + block_slot: 3241381, + network_id: 1, }; - match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { - Ok(()) => panic!("All inputs must have a witness signature."), + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "All inputs must have a witness signature"), Err(err) => match err { - ValidationError::MissingWitness => (), - _ => panic!("Unexpected error ({:?}).", err), + Byron(MissingWitness) => (), + _ => panic!("Unexpected error ({:?})", err), }, } } @@ -383,12 +375,7 @@ mod byron_tests { // wrong. fn wrong_signature() { let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); - let mut mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); - let utxos: UTxOs = mk_utxo_for_single_input_tx( - &mtxp.transaction, - String::from(include_str!("../../test_data/byron1.address")), - 19999000000, - ); + let mut mtxp: MintedTxPayload = tx_from_cbor(&cbor_bytes); // Modify signature in witness let new_wit: Twit = match mtxp.witness[0].clone() { Twit::PkWitness(CborWrap((pk, _))) => { @@ -402,26 +389,40 @@ mod byron_tests { match encode(new_witnesses, &mut tx_buf) { Ok(_) => (), - Err(err) => panic!("Unable to encode Tx ({:?}).", err), + Err(err) => panic!("Unable to encode Tx ({:?})", err), }; - - mtxp.witness = Decode::decode(&mut Decoder::new(tx_buf.as_slice()), &mut ()).unwrap(); - + mtxp.witness = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let metx: MultiEraTx = MultiEraTx::from_byron(&mtxp); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtxp.transaction, + String::from(include_str!("../../test_data/byron1.address")), + 19999000000, + ); let env: Environment = Environment { prot_params: MultiEraProtParams::Byron(ByronProtParams { - min_fees_const: 155381, - min_fees_factor: 44, + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, max_tx_size: 4096, }), prot_magic: 764824073, + block_slot: 3241381, + network_id: 1, }; - - match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { - Ok(()) => panic!("Witness signature should verify the transaction."), + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Witness signature should verify the transaction"), Err(err) => match err { - ValidationError::WrongSignature => (), - _ => panic!("Unexpected error ({:?}).", err), + Byron(WrongSignature) => (), + _ => panic!("Unexpected error ({:?})", err), }, } } } + +// Helper functions. +fn add_to_utxo<'a>(utxos: &mut UTxOs<'a>, tx_in: TxIn, tx_out: TxOut) { + let multi_era_in: MultiEraInput = MultiEraInput::Byron(Box::new(Cow::Owned(tx_in))); + let multi_era_out: MultiEraOutput = MultiEraOutput::Byron(Box::new(Cow::Owned(tx_out))); + utxos.insert(multi_era_in, multi_era_out); +} diff --git a/pallas-applying/tests/shelley_ma.rs b/pallas-applying/tests/shelley_ma.rs new file mode 100644 index 0000000..e1f489b --- /dev/null +++ b/pallas-applying/tests/shelley_ma.rs @@ -0,0 +1,727 @@ +use std::borrow::Cow; + +use pallas_addresses::{Address, Network, ShelleyAddress}; +use pallas_applying::{ + types::{ + Environment, FeePolicy, MultiEraProtParams, ShelleyMAError::*, ShelleyProtParams, + ValidationError::*, + }, + validate, UTxOs, +}; +use pallas_codec::{ + minicbor::{ + decode::{Decode, Decoder}, + encode, + }, + utils::Bytes, +}; +use pallas_crypto::hash::Hash; +use pallas_primitives::alonzo::{ + MintedTx, MintedWitnessSet, TransactionBody, TransactionInput, TransactionOutput, VKeyWitness, + Value, +}; +use pallas_traverse::{Era, MultiEraInput, MultiEraOutput, MultiEraTx}; + +#[cfg(test)] +mod shelley_tests { + use super::*; + + fn cbor_to_bytes(input: &str) -> Vec { + hex::decode(input).unwrap() + } + + fn minted_tx_from_cbor<'a>(tx_cbor: &'a Vec) -> MintedTx<'a> { + pallas_codec::minicbor::decode::(&tx_cbor[..]).unwrap() + } + + // Careful: this function assumes tx_body has exactly one input. + fn mk_utxo_for_single_input_tx<'a>( + tx_body: &TransactionBody, + address: String, + amount: Value, + datum_hash: Option>, + ) -> UTxOs<'a> { + let tx_ins: &Vec = &tx_body.inputs; + assert_eq!(tx_ins.len(), 1, "Unexpected number of inputs"); + let tx_in: TransactionInput = tx_ins.first().unwrap().clone(); + let address_bytes: Bytes = match hex::decode(address) { + Ok(bytes_vec) => Bytes::from(bytes_vec), + _ => panic!("Unable to decode input address"), + }; + let tx_out: TransactionOutput = TransactionOutput { + address: address_bytes, + amount, + datum_hash, + }; + let mut utxos: UTxOs = UTxOs::new(); + add_to_utxo(&mut utxos, tx_in, tx_out); + utxos + } + + #[test] + // Transaction hash: 50eba65e73c8c5f7b09f4ea28cf15dce169f3d1c322ca3deff03725f51518bb2 + fn successful_mainnet_shelley_tx() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley1.tx")); + let mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley1.address")), + Value::Coin(2332267427205), + None, + ); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 5281340, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => (), + Err(err) => assert!(false, "Unexpected error ({:?})", err), + } + } + + #[test] + // Transaction hash: 4a3f86762383f1d228542d383ae7ac89cf75cf7ff84dec8148558ea92b0b92d0 + fn successful_mainnet_shelley_tx_with_script() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley2.tx")); + let mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley2.address")), + Value::Coin(2000000), + None, + ); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 17584925, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => (), + Err(err) => assert!(false, "Unexpected error ({:?})", err), + } + } + + #[test] + // Transaction hash: c220e20cc480df9ce7cd871df491d7390c6a004b9252cf20f45fc3c968535b4a + fn successful_mainnet_shelley_tx_with_metadata() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley3.tx")); + let mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley3.address")), + Value::Coin(10000000), + None, + ); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 5860488, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => (), + Err(err) => assert!(false, "Unexpected error ({:?})", err), + } + } + + #[test] + // Transaction hash: b7b1046d1787ac6917f5bb5841e73b3f4bef8f0a6bf692d05ef18e1db9c3f519 + fn successful_mainnet_mary_tx_with_minting() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/mary1.tx")); + let mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Mary); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/mary1.address")), + Value::Coin(3500000), + None, + ); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 24381863, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => (), + Err(err) => assert!(false, "Unexpected error ({:?})", err), + } + } + + #[test] + // All inputs are removed. + fn empty_ins() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley1.tx")); + let mut mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley1.address")), + Value::Coin(2332267427205), + None, + ); + // Clear the set of inputs in the transaction. + let mut tx_body: TransactionBody = (*mtx.transaction_body).clone(); + tx_body.inputs = Vec::new(); + let mut tx_buf: Vec = Vec::new(); + match encode(tx_body, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?})", err), + }; + mtx.transaction_body = + Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 5281340, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Inputs set should not be empty"), + Err(err) => match err { + Shelley(TxInsEmpty) => (), + _ => assert!(false, "Unexpected error ({:?})", err), + }, + } + } + + #[test] + // The UTxO set is empty. + fn unfound_utxo() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley1.tx")); + let mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let utxos: UTxOs = UTxOs::new(); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 5281340, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "All inputs must be within the UTxO set"), + Err(err) => match err { + Shelley(InputNotInUTxO) => (), + _ => assert!(false, "Unexpected error ({:?})", err), + }, + } + } + + #[test] + // Time-to-live is missing. + fn missing_ttl() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley1.tx")); + let mut mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley1.address")), + Value::Coin(2332267427205), + None, + ); + let mut tx_body: TransactionBody = (*mtx.transaction_body).clone(); + tx_body.ttl = None; + let mut tx_buf: Vec = Vec::new(); + match encode(tx_body, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?})", err), + }; + mtx.transaction_body = + Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 5281340, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "TTL must always be present in Shelley transactions"), + Err(err) => match err { + Shelley(AlonzoCompNotShelley) => (), + _ => assert!(false, "Unexpected error ({:?})", err), + }, + } + } + + #[test] + // Transaction's time-to-live is before block slot. + fn ttl_exceeded() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley1.tx")); + let mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley1.address")), + Value::Coin(2332267427205), + None, + ); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 9999999, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "TTL cannot be exceeded"), + Err(err) => match err { + Shelley(TTLExceeded) => (), + _ => assert!(false, "Unexpected error ({:?})", err), + }, + } + } + + #[test] + // Transaction size exceeds max limit (namely, 0). + fn max_tx_size_exceeded() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley1.tx")); + let mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley1.address")), + Value::Coin(2332267427205), + None, + ); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 0, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 5281340, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Tx size exceeds max limit"), + Err(err) => match err { + Shelley(MaxTxSizeExceeded) => (), + _ => assert!(false, "Unexpected error ({:?})", err), + }, + } + } + + #[test] + // Min lovelace per UTxO is too high (10000000000000 lovelace against 2332262258756 lovelace in + // transaction output). + fn output_below_min_lovelace() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley1.tx")); + let mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley1.address")), + Value::Coin(2332267427205), + None, + ); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 10000000000000, + }), + prot_magic: 764824073, + block_slot: 5281340, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Output amount must be above min lovelace value"), + Err(err) => match err { + Shelley(MinLovelaceUnreached) => (), + _ => assert!(false, "Unexpected error ({:?})", err), + }, + } + } + + #[test] + // The "preservation of value" property doesn't hold - the fee is reduced by exactly 1. + fn preservation_of_value() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley1.tx")); + let mut mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let mut tx_body: TransactionBody = (*mtx.transaction_body).clone(); + tx_body.fee = tx_body.fee - 1; + let mut tx_buf: Vec = Vec::new(); + match encode(tx_body, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?})", err), + }; + mtx.transaction_body = + Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley1.address")), + Value::Coin(2332267427205), + None, + ); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 5281340, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Preservation of value property doesn't hold"), + Err(err) => match err { + Shelley(PreservationOfValue) => (), + _ => assert!(false, "Unexpected error ({:?})", err), + }, + } + } + + #[test] + // Fee policy imposes higher fees on the transaction. + fn fee_below_minimum() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley1.tx")); + let mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley1.address")), + Value::Coin(2332267427205), + None, + ); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 70, // This value was 44 during Shelley on mainnet. + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 5281340, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Fee should not be below minimum"), + Err(err) => match err { + Shelley(FeesBelowMin) => (), + _ => assert!(false, "Unexpected error ({:?})", err), + }, + } + } + + #[test] + // One of the output's address network ID is changed from the mainnet value to the testnet one. + fn wrong_network_id() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley1.tx")); + let mut mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + // Modify the first output address. + let mut tx_body: TransactionBody = (*mtx.transaction_body).clone(); + let (first_output, rest): (&TransactionOutput, &[TransactionOutput]) = + (&tx_body.outputs).split_first().unwrap(); + let addr: ShelleyAddress = + match Address::from_bytes(&Vec::::from(first_output.address.clone())) { + Ok(Address::Shelley(sa)) => sa, + Ok(_) => panic!("Decoded output address and found the wrong era"), + Err(e) => panic!("Unable to parse output address ({:?})", e), + }; + let altered_address: ShelleyAddress = ShelleyAddress::new( + Network::Testnet, + addr.payment().clone(), + addr.delegation().clone(), + ); + let altered_output: TransactionOutput = TransactionOutput { + address: Bytes::from(altered_address.to_vec()), + amount: first_output.amount.clone(), + datum_hash: first_output.datum_hash, + }; + let mut new_outputs = Vec::from(rest); + new_outputs.insert(0, altered_output); + tx_body.outputs = new_outputs; + let mut tx_buf: Vec = Vec::new(); + match encode(tx_body, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?})", err), + }; + mtx.transaction_body = + Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 5281340, + network_id: 1, + }; + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley1.address")), + Value::Coin(2332267427205), + None, + ); + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Output with wrong network ID should be rejected"), + Err(err) => match err { + Shelley(WrongNetworkID) => (), + _ => assert!(false, "Unexpected error ({:?})", err), + }, + } + } + + #[test] + // Like successful_mainnet_shelley_tx_with_metadata (hash: + // c220e20cc480df9ce7cd871df491d7390c6a004b9252cf20f45fc3c968535b4a) + fn auxiliary_data_removed() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley3.tx")); + let mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley3.address")), + Value::Coin(10000000), + None, + ); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 5860488, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => (), + Err(err) => assert!(false, "Unexpected error ({:?})", err), + } + } + + #[test] + // Like successful_mainnet_shelley_tx (hash: + // 50eba65e73c8c5f7b09f4ea28cf15dce169f3d1c322ca3deff03725f51518bb2), but the verification-key + // witness is removed. + fn missing_vk_witness() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley1.tx")); + let mut mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + // Modify the first output address. + let mut tx_wits: MintedWitnessSet = (*mtx.transaction_witness_set).clone(); + tx_wits.vkeywitness = Some(Vec::new()); + let mut tx_buf: Vec = Vec::new(); + match encode(tx_wits, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?})", err), + }; + mtx.transaction_witness_set = + Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 5281340, + network_id: 1, + }; + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley1.address")), + Value::Coin(2332267427205), + None, + ); + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Missing verification key witness"), + Err(err) => match err { + Shelley(MissingVKWitness) => (), + _ => assert!(false, "Unexpected error ({:?})", err), + }, + } + } + + #[test] + // Like successful_mainnet_shelley_tx (hash: + // 50eba65e73c8c5f7b09f4ea28cf15dce169f3d1c322ca3deff03725f51518bb2), but the signature inside + // the verification-key witness is changed. + fn vk_witness_changed() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley1.tx")); + let mut mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + // Modify the first output address. + let mut tx_wits: MintedWitnessSet = (*mtx.transaction_witness_set).clone(); + let mut wit: VKeyWitness = tx_wits.vkeywitness.clone().unwrap().pop().unwrap(); + let mut sig_as_vec: Vec = wit.signature.to_vec(); + sig_as_vec.pop(); + sig_as_vec.push(0u8); + wit.signature = Bytes::from(sig_as_vec); + tx_wits.vkeywitness = Some(Vec::from([wit])); + let mut tx_buf: Vec = Vec::new(); + match encode(tx_wits, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?})", err), + }; + mtx.transaction_witness_set = + Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 5281340, + network_id: 1, + }; + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley1.address")), + Value::Coin(2332267427205), + None, + ); + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Missing verification key witness"), + Err(err) => match err { + Shelley(WrongSignature) => (), + _ => assert!(false, "Unexpected error ({:?})", err), + }, + } + } + + #[test] + // Like successful_mainnet_shelley_tx_with_script(hash: + // 4a3f86762383f1d228542d383ae7ac89cf75cf7ff84dec8148558ea92b0b92d0), but the native-script + // witness is removed. + fn missing_native_script_witness() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley2.tx")); + let mut mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + // Modify the first output address. + let mut tx_wits: MintedWitnessSet = (*mtx.transaction_witness_set).clone(); + tx_wits.native_script = Some(Vec::new()); + let mut tx_buf: Vec = Vec::new(); + match encode(tx_wits, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?})", err), + }; + mtx.transaction_witness_set = + Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Shelley); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 4096, + min_lovelace: 1000000, + }), + prot_magic: 764824073, + block_slot: 5281340, + network_id: 1, + }; + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtx.transaction_body, + String::from(include_str!("../../test_data/shelley2.address")), + Value::Coin(2000000), + None, + ); + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Missing native script witness"), + Err(err) => match err { + Shelley(MissingScriptWitness) => (), + _ => assert!(false, "Unexpected error ({:?})", err), + }, + } + } +} + +// Helper functions. +fn add_to_utxo<'a>(utxos: &mut UTxOs<'a>, tx_in: TransactionInput, tx_out: TransactionOutput) { + let multi_era_in: MultiEraInput = MultiEraInput::AlonzoCompatible(Box::new(Cow::Owned(tx_in))); + let multi_era_out: MultiEraOutput = + MultiEraOutput::AlonzoCompatible(Box::new(Cow::Owned(tx_out))); + utxos.insert(multi_era_in, multi_era_out); +} diff --git a/test_data/mary1.address b/test_data/mary1.address new file mode 100644 index 0000000..b888036 --- /dev/null +++ b/test_data/mary1.address @@ -0,0 +1 @@ +611489ac0c22c04abc9c6de7f95d71e1ba2c95c9b4e2f6f2900f682285 \ No newline at end of file diff --git a/test_data/mary1.tx b/test_data/mary1.tx new file mode 100644 index 0000000..fcda6eb --- /dev/null +++ b/test_data/mary1.tx @@ -0,0 +1 @@ +84a5008182582027c39310c79aa7e37d1fba4e455698e9c918b41183b146a38acfcc0bc223792000018182581d611489ac0c22c04abc9c6de7f95d71e1ba2c95c9b4e2f6f2900f682285821a0032a7fba1581cf523573c4df900cf0fe16312aa7445877098b2a001dced3cc1283358a14a546f6d61746f436f696e1a000f4240021a0002bfe5031a01740de209a1581cf523573c4df900cf0fe16312aa7445877098b2a001dced3cc1283358a14a546f6d61746f436f696e1a000f4240a200828258204cbc1c2c8a6bd40ec53ef8a09cf1a2f78c301cb1185d78c5740b574399a2a71058404e0176cd7004cfe6163fce238836cf67a8fe49395d1beaa5500b05b23395482879bc9e01493429798516b9ffec98135f518943bf13513e85b6a659d290627806825820d3a8b343712f3734f39f95f01080c53514e070f2f115b5c9234ee5f7b554745b58405e35fe7f6e7f2402757c7aff566c4190a6031c47ff6dc8fc0e01b3a8323d1ecf41175c43877e34664c0f1c331a96222498f70586361d9c01f7218a26495a0d03018182018282051a01740de28200581c1d53b024073333d411cb1585adfe5f02cd2410f65e33b666d0c9633cf5f6 \ No newline at end of file diff --git a/test_data/shelley1.address b/test_data/shelley1.address new file mode 100644 index 0000000..125a934 --- /dev/null +++ b/test_data/shelley1.address @@ -0,0 +1 @@ +0129bb156d52d014bb444a14138cbee36044c6faed37d0c2d49d2358315c465cbf8c5536970e8a29bb7adcda0d663b20007d481813694c64ef \ No newline at end of file diff --git a/test_data/shelley1.tx b/test_data/shelley1.tx new file mode 100644 index 0000000..34fac60 --- /dev/null +++ b/test_data/shelley1.tx @@ -0,0 +1 @@ +84a4008182582031cf218c94a63e2a5d1f054751c062ada6add8ae2fbe75dabaf2fe2cea9a2619000182825839019c1bb4c1b426ef53afdfc6f6011b6a8ca22b0aaa8330080cca5ab64e5c465cbf8c5536970e8a29bb7adcda0d663b20007d481813694c64ef1b0000021f05a9d84482583901988de808740f48086ae4a372f417a33eb1a4c22d24a88f30efc304b0c9fe643d0170b639353141c6cbff9dfc9eddcba34c8fbac3a4d4d83c1a004c4b40021a00029201031a0050b248a10081825820a8beae2d04b36fecbfec7eaf121656932929e150aa58ae1ff7091571872d96d15840a904723932843fc56cc57afc44d766d229413095d0027360879f09e04a867d19fdc7f8d464c75cde8cbb209760d665e18903f330d74116e188705d66c0accd02f5f6 \ No newline at end of file diff --git a/test_data/shelley2.address b/test_data/shelley2.address new file mode 100644 index 0000000..a3912a1 --- /dev/null +++ b/test_data/shelley2.address @@ -0,0 +1 @@ +7165c197d565e88a20885e535f93755682444d3c02fd44dd70883fe89e \ No newline at end of file diff --git a/test_data/shelley2.tx b/test_data/shelley2.tx new file mode 100644 index 0000000..1d0abc6 --- /dev/null +++ b/test_data/shelley2.tx @@ -0,0 +1 @@ +84a40081825820e7db1f809fcc21d3dd108ced6218bf0f0cbb6a0f679f848ff1790b68d3a35872000181825839010c57a4aa08aaa7c42b45e4e9490151e2665dbb7d374e795ad5be5e4960562a0d213c675c2b84ee0e34eb377d4abbe82a4c256a0708baac251a0016e360021a0007a120031a010c59f8a200838258205df1be8b0071123c982a94c19c0c06485dbe9271e4381e8cf4fc2ed554ac133f5840c271a9d652ae95e6a8a5117c5026369235182da7e4fe040b02465089e5e2caf05063cf8b61601e6f6074c03f700bacaadcd62483c48d66a5f8d418a7c27c4b01825820403171966fadb1ce9b26852cb74018a04bc031a4aee92be39702b18efd75e058584039395858906ec9ab7540e79022b25a1f3bdfece09f9e2f36254eb5abe625b72dbd8179ece4c9fc1d537afce95b67d8095d29e1f3c50de4ecc30fd67e1ba440048258206311da054c5dfa3ac53c9fc3be859bd322f0712f7e093596fa5f6de031d95acd58403d7deba60f80a03f5bf172c1699f07ecef557d9509551cbe8d2b9000e903c3e3f60bb127c9c0b4cd5df84c01791b98e10fe19209088d0b085c74702b25914a0d01818201838200581ca96da581c39549aeda81f539ac3940ac0cb53657e774ca7e68f15ed98200581cccfcb3fed004562be1354c837a4a4b9f4b1c2b6705229efeedd12d4d8200581c74fcd61aecebe36aa6b6cd4314027282fa4b41c3ce8af17d9b77d0d1f5f6 \ No newline at end of file diff --git a/test_data/shelley3.address b/test_data/shelley3.address new file mode 100644 index 0000000..78d07e6 --- /dev/null +++ b/test_data/shelley3.address @@ -0,0 +1 @@ +61c96001f4a4e10567ac18be3c47663a00a858f51c56779e94993d30ef \ No newline at end of file diff --git a/test_data/shelley3.tx b/test_data/shelley3.tx new file mode 100644 index 0000000..5b187fd --- /dev/null +++ b/test_data/shelley3.tx @@ -0,0 +1 @@ +84a500818258205b06f6ea129a404d5bc610880be35376625a8f7f11773bf79db1889eb3bb87eb00018182581d61c96001f4a4e10567ac18be3c47663a00a858f51c56779e94993d30ef1a0095e957021a0002ad29031a005991b0075820c2d2b42fbacf30eeddab1447f525297eec0ab134f8cddd2025a075c69d57e4bca100818258204251d746864839409bc2bb6dfbb680c503c3a2613dba0ac55c6791eaebd9ad84584057e649e46b1711bfd45cb2ae0e4ecb8c863e5c261545f0ec96fe6ee3fc8dd5b36106fd13d21e643c34e04c58b18759afaca58f990060b4342dd7369bd11b1d06f5a101a368766f7465725f69646f3132336162633030306362613332316662616c6c6f74a36669737375653163796573666973737565336e416c7068612043656e746175726966697373756532626e6f67766f74655f696469616263313233303030 \ No newline at end of file