From 68b46c36a80775e3511b38447e870ccde44b04b3 Mon Sep 17 00:00:00 2001 From: Maico Leberle Date: Thu, 9 Nov 2023 14:50:46 -0300 Subject: [PATCH] feat(applying): add remaining validations for Byron era (#325) --- pallas-applying/Cargo.toml | 4 + pallas-applying/src/byron.rs | 223 ++++++++++++++- pallas-applying/src/lib.rs | 20 +- pallas-applying/src/types.rs | 34 ++- pallas-applying/tests/README.md | 20 ++ pallas-applying/tests/byron.rs | 466 +++++++++++++++++++------------- test_data/byron1.tx | 1 + 7 files changed, 553 insertions(+), 215 deletions(-) create mode 100644 pallas-applying/tests/README.md create mode 100644 test_data/byron1.tx diff --git a/pallas-applying/Cargo.toml b/pallas-applying/Cargo.toml index 78e6335e..a459f944 100644 --- a/pallas-applying/Cargo.toml +++ b/pallas-applying/Cargo.toml @@ -13,8 +13,12 @@ authors = ["Maico Leberle "] doctest = false [dependencies] +pallas-addresses = { path = "../pallas-addresses" } pallas-codec = { path = "../pallas-codec" } pallas-crypto = { path = "../pallas-crypto" } pallas-primitives = { path = "../pallas-primitives" } pallas-traverse = { path = "../pallas-traverse" } rand = "0.8" + +[dev-dependencies] +hex = "0.4" diff --git a/pallas-applying/src/byron.rs b/pallas-applying/src/byron.rs index 67136d89..ad604eef 100644 --- a/pallas-applying/src/byron.rs +++ b/pallas-applying/src/byron.rs @@ -1,19 +1,43 @@ //! Utilities required for Byron-era transaction validation. -use crate::types::{ByronProtParams, MultiEraInput, UTxOs, ValidationError, ValidationResult}; +use std::borrow::Cow; -use pallas_primitives::byron::{MintedTxPayload, Tx}; +use crate::types::{ + ByronProtParams, MultiEraInput, MultiEraOutput, SigningTag, UTxOs, ValidationError, + ValidationResult, +}; + +use pallas_addresses::byron::{ + AddrAttrs, AddrType, AddressId, AddressPayload, ByronAddress, SpendingData, +}; +use pallas_codec::{ + minicbor::{encode, Encoder}, + utils::CborWrap, +}; +use pallas_crypto::{ + hash::Hash, + key::ed25519::{PublicKey, Signature}, +}; +use pallas_primitives::byron::{ + Address, MintedTxPayload, PubKey, Signature as ByronSignature, Twit, Tx, TxIn, TxOut, +}; +use pallas_traverse::OriginalHash; -// TODO: implement missing validation rules. pub fn validate_byron_tx( mtxp: &MintedTxPayload, utxos: &UTxOs, - _prot_pps: &ByronProtParams, + prot_pps: &ByronProtParams, + prot_magic: &u32, ) -> ValidationResult { let tx: &Tx = &mtxp.transaction; + let size: u64 = get_tx_size(tx)?; check_ins_not_empty(tx)?; check_outs_not_empty(tx)?; - check_ins_in_utxos(tx, utxos) + check_ins_in_utxos(tx, utxos)?; + check_outs_have_lovelace(tx)?; + 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 { @@ -38,3 +62,192 @@ fn check_ins_in_utxos(tx: &Tx, utxos: &UTxOs) -> ValidationResult { } Ok(()) } + +fn check_outs_have_lovelace(tx: &Tx) -> ValidationResult { + for output in tx.outputs.iter() { + if output.amount == 0 { + return Err(ValidationError::OutputWithoutLovelace); + } + } + Ok(()) +} + +fn check_fees(tx: &Tx, size: &u64, utxos: &UTxOs, prot_pps: &ByronProtParams) -> ValidationResult { + let mut inputs_balance: u64 = 0; + for input in tx.inputs.iter() { + match utxos + .get(&MultiEraInput::from_byron(input)) + .and_then(MultiEraOutput::as_byron) + { + Some(byron_utxo) => inputs_balance += byron_utxo.amount, + None => return Err(ValidationError::UnableToComputeFees), + } + } + let mut outputs_balance: u64 = 0; + for output in tx.outputs.iter() { + 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; + if total_balance < min_fees { + return Err(ValidationError::FeesBelowMin); + } + Ok(()) +} + +fn check_size(size: &u64, prot_pps: &ByronProtParams) -> ValidationResult { + if *size > prot_pps.max_tx_size { + return Err(ValidationError::MaxTxSizeExceeded); + } + Ok(()) +} + +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), + } +} + +pub enum TaggedSignature<'a> { + PkWitness(&'a ByronSignature), + RedeemWitness(&'a ByronSignature), +} + +fn check_witnesses(mtxp: &MintedTxPayload, utxos: &UTxOs, prot_magic: &u32) -> ValidationResult { + let tx: &Tx = &mtxp.transaction; + let tx_hash: Hash<32> = mtxp.transaction.original_hash(); + let witnesses: Vec<(&PubKey, TaggedSignature)> = tag_witnesses(&mtxp.witness)?; + let tx_inputs: &Vec = &tx.inputs; + for input in tx_inputs { + let tx_out: &TxOut = find_tx_out(input, utxos)?; + let (pub_key, sign): (&PubKey, &TaggedSignature) = find_raw_witness(tx_out, &witnesses)?; + let public_key: PublicKey = get_verification_key(pub_key); + 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); + } + } + Ok(()) +} + +fn tag_witnesses(wits: &[Twit]) -> Result, ValidationError> { + let mut res: Vec<(&PubKey, TaggedSignature)> = Vec::new(); + for wit in wits.iter() { + match wit { + Twit::PkWitness(CborWrap((pk, sig))) => { + res.push((pk, TaggedSignature::PkWitness(sig))); + } + Twit::RedeemWitness(CborWrap((pk, sig))) => { + res.push((pk, TaggedSignature::RedeemWitness(sig))); + } + _ => return Err(ValidationError::UnableToProcessWitnesses), + } + } + Ok(res) +} + +fn find_tx_out<'a>(input: &'a TxIn, utxos: &'a UTxOs) -> Result<&'a TxOut, ValidationError> { + let key: MultiEraInput = MultiEraInput::Byron(Box::new(Cow::Borrowed(input))); + utxos + .get(&key) + .ok_or(ValidationError::InputMissingInUTxO)? + .as_byron() + .ok_or(ValidationError::InputMissingInUTxO) +} + +fn find_raw_witness<'a>( + tx_out: &TxOut, + witnesses: &'a Vec<(&'a PubKey, TaggedSignature<'a>)>, +) -> Result<(&'a PubKey, &'a TaggedSignature<'a>), ValidationError> { + let address: ByronAddress = mk_byron_address(&tx_out.address); + let addr_payload: AddressPayload = address + .decode() + .map_err(|_| ValidationError::UnableToProcessWitnesses)?; + let root: AddressId = addr_payload.root; + let attr: AddrAttrs = addr_payload.attributes; + let addr_type: AddrType = addr_payload.addrtype; + for (pub_key, sign) in witnesses { + 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), + } + } + } + Err(ValidationError::MissingWitness) +} + +fn mk_byron_address(addr: &Address) -> ByronAddress { + ByronAddress::new((*addr.payload.0).as_slice(), addr.crc) +} + +fn redeems( + pub_key: &PubKey, + sign: &TaggedSignature, + root: &AddressId, + attrs: &AddrAttrs, + addr_type: &AddrType, +) -> bool { + let spending_data: SpendingData = mk_spending_data(pub_key, addr_type); + let hash_to_check: AddressId = + AddressPayload::hash_address_id(addr_type, &spending_data, attrs); + hash_to_check == *root && convert_to_addr_type(sign) == *addr_type +} + +fn convert_to_addr_type(sign: &TaggedSignature) -> AddrType { + match sign { + TaggedSignature::PkWitness(_) => AddrType::PubKey, + TaggedSignature::RedeemWitness(_) => AddrType::Redeem, + } +} + +fn mk_spending_data(pub_key: &PubKey, addr_type: &AddrType) -> SpendingData { + match addr_type { + AddrType::PubKey => SpendingData::PubKey(pub_key.clone()), + AddrType::Redeem => SpendingData::Redeem(pub_key.clone()), + _ => unreachable!(), + } +} + +fn get_verification_key(pk: &PubKey) -> PublicKey { + let mut trunc_len: [u8; PublicKey::SIZE] = [0; PublicKey::SIZE]; + trunc_len.copy_from_slice(&pk.as_slice()[0..PublicKey::SIZE]); + From::<[u8; PublicKey::SIZE]>::from(trunc_len) +} + +fn get_data_to_verify( + sign: &TaggedSignature, + prot_magic: &u32, + tx_hash: &Hash<32>, +) -> Result, ValidationError> { + let buff: &mut Vec = &mut Vec::new(); + let mut enc: Encoder<&mut Vec> = Encoder::new(buff); + match sign { + TaggedSignature::PkWitness(_) => { + enc.encode(SigningTag::Tx as u64) + .map_err(|_| ValidationError::UnableToProcessWitnesses)?; + } + TaggedSignature::RedeemWitness(_) => { + enc.encode(SigningTag::RedeemTx as u64) + .map_err(|_| ValidationError::UnableToProcessWitnesses)?; + } + } + enc.encode(prot_magic) + .map_err(|_| ValidationError::UnableToProcessWitnesses)?; + enc.encode(tx_hash) + .map_err(|_| ValidationError::UnableToProcessWitnesses)?; + Ok(enc.into_writer().clone()) +} + +fn get_signature(tagged_signature: &TaggedSignature<'_>) -> Signature { + let inner_sig = match tagged_signature { + TaggedSignature::PkWitness(sign) => sign, + TaggedSignature::RedeemWitness(sign) => sign, + }; + let mut trunc_len: [u8; Signature::SIZE] = [0; Signature::SIZE]; + trunc_len.copy_from_slice(inner_sig.as_slice()); + From::<[u8; Signature::SIZE]>::from(trunc_len) +} diff --git a/pallas-applying/src/lib.rs b/pallas-applying/src/lib.rs index b1f00c62..78f48b3a 100644 --- a/pallas-applying/src/lib.rs +++ b/pallas-applying/src/lib.rs @@ -7,17 +7,17 @@ use byron::validate_byron_tx; use pallas_traverse::{MultiEraTx, MultiEraTx::Byron as ByronTxPayload}; -pub use types::{ - MultiEraProtParams, MultiEraProtParams::Byron as ByronProtParams, UTxOs, ValidationResult, -}; +pub use types::{Environment, MultiEraProtParams, UTxOs, ValidationResult}; -pub fn validate( - metx: &MultiEraTx, - utxos: &UTxOs, - prot_pps: &MultiEraProtParams, -) -> ValidationResult { - match (metx, prot_pps) { - (ByronTxPayload(mtxp), ByronProtParams(bpp)) => validate_byron_tx(mtxp, utxos, bpp), +pub fn validate(metx: &MultiEraTx, utxos: &UTxOs, env: &Environment) -> ValidationResult { + match (metx, env) { + ( + ByronTxPayload(mtxp), + Environment { + prot_params: MultiEraProtParams::Byron(bpp), + prot_magic, + }, + ) => validate_byron_tx(mtxp, utxos, bpp, prot_magic), // TODO: implement the rest of the eras. _ => Ok(()), } diff --git a/pallas-applying/src/types.rs b/pallas-applying/src/types.rs index a2090086..a6b97125 100644 --- a/pallas-applying/src/types.rs +++ b/pallas-applying/src/types.rs @@ -1,29 +1,51 @@ //! Base types used for validating transactions in each era. -use std::{borrow::Cow, collections::HashMap}; +use std::collections::HashMap; pub use pallas_traverse::{MultiEraInput, MultiEraOutput}; pub type UTxOs<'b> = HashMap, MultiEraOutput<'b>>; -// TODO: add a field for each protocol parameter in the Byron era. #[derive(Debug, Clone)] -pub struct ByronProtParams; +pub struct ByronProtParams { + pub min_fees_const: u64, + pub min_fees_factor: u64, + pub max_tx_size: u64, +} // TODO: add variants for the other eras. #[derive(Debug)] #[non_exhaustive] -pub enum MultiEraProtParams<'b> { - Byron(Box>), +pub enum MultiEraProtParams { + Byron(ByronProtParams), +} + +#[derive(Debug)] +pub struct Environment { + pub prot_params: MultiEraProtParams, + pub prot_magic: u32, +} + +#[non_exhaustive] +pub enum SigningTag { + Tx = 0x01, + RedeemTx = 0x02, } -// TODO: replace this generic variant with validation-rule-specific ones. #[derive(Debug)] #[non_exhaustive] pub enum ValidationError { InputMissingInUTxO, TxInsEmpty, TxOutsEmpty, + OutputWithoutLovelace, + UnknownTxSize, + UnableToComputeFees, + FeesBelowMin, + MaxTxSizeExceeded, + UnableToProcessWitnesses, + MissingWitness, + WrongSignature, } pub type ValidationResult = Result<(), ValidationError>; diff --git a/pallas-applying/tests/README.md b/pallas-applying/tests/README.md new file mode 100644 index 00000000..2912f2ab --- /dev/null +++ b/pallas-applying/tests/README.md @@ -0,0 +1,20 @@ +# Testing framework documentation + +## Execution +Starting at the root of the repository, simply go to *pallas-applying* and run `cargo test`. + + +## Explanations +*pallas-applying/tests/byron.rs* contains multiple unit tests for validation on the Byron era. + +The first one, **suceessful_mainnet_tx**, is a positive unit test. It takes the CBOR of a mainnet transaction. Namely, the one whose hash is `a06e5a0150e09f8983be2deafab9e04afc60d92e7110999eb672c903343f1e26`, which can be viewed on Cardano Explorer [here](https://cexplorer.io/tx/a06e5a0150e09f8983be2deafab9e04afc60d92e7110999eb672c903343f1e26). Such a transaction has a single input which is added to the UTxO, prior to validation, by associating it to a transaction output sitting at its real (mainnet) address. This information was taken from Cardano Explorer as well, following the address link of the only input to the transaction, and taking its raw address CBOR content. + +Then comes a series of negative unit tests, namely: +- **empty_ins** takes the mainnet transaction, removes its input, and calls validation on it. +- **empty_outs** is analogous to the **empty_ins** test, removing all outputs instead. +- **unfound_utxo** takes the mainnet transaction and calls validation on it without a proper UTxO containing an entry for its input. +- **output_without_lovelace** takes the mainnet transaction and modifies its output by removing all of its lovelace. +- **not_enough_fees** takes the mainnet transaction and calls validation on it using wrong protocol parameters, which requiere that the transaction pay a higher fee than the one actually paid. +- **tx_size_exceeds_max** takes the mainnet transaction and calls validation on it using wrong protocol parameters, which only allow transactions of a size smaller than that of the transaction. +- **missing_witness** takes the mainnet transaction, removes its witness, and calls validation on it. +- **wrong_signature** takes the mainnet transaction, alters the content of its witness, and calls validation on it. diff --git a/pallas-applying/tests/byron.rs b/pallas-applying/tests/byron.rs index aac041c5..5c45cd21 100644 --- a/pallas-applying/tests/byron.rs +++ b/pallas-applying/tests/byron.rs @@ -1,8 +1,7 @@ -use rand::Rng; use std::{borrow::Cow, vec::Vec}; use pallas_applying::{ - types::{ByronProtParams, MultiEraProtParams, ValidationError}, + types::{ByronProtParams, Environment, MultiEraProtParams, ValidationError}, validate, UTxOs, ValidationResult, }; use pallas_codec::{ @@ -11,77 +10,89 @@ use pallas_codec::{ decode::{Decode, Decoder}, encode, }, - utils::{CborWrap, EmptyMap, KeepRaw, MaybeIndefArray, TagWrap}, -}; -use pallas_crypto::hash::Hash; -use pallas_primitives::byron::{ - Address, Attributes, MintedTxPayload as ByronTxPayload, Tx as ByronTx, TxId as ByronTxId, - TxIn as ByronTxIn, TxOut as ByronTxOut, Witnesses as ByronWitnesses, + utils::{CborWrap, KeepRaw, MaybeIndefArray, TagWrap}, }; +use pallas_primitives::byron::{Address, MintedTxPayload, Twit, Tx, TxIn, TxOut, Witnesses}; use pallas_traverse::{MultiEraInput, MultiEraOutput, MultiEraTx}; #[cfg(test)] mod byron_tests { use super::*; - #[test] - // Note that: - // i) the transaction input contains 100000 lovelace, - // ii) the minimum_fee_constant protocol parameter is 7, - // iii) the minimum_fee_factor protocol parameter is 11, and - // iv) the size of the transaction is 82 bytes—it is easy to verify - // that 82 == pallas_applying::get_byron_tx_size(tx). - // The expected fees are therefore 7 + 11 * 82 = 909 lovelace, which is why - // the output contains 100000 - 909 = 99091 lovelace. - fn successful_case() { - let protocol_params: ByronProtParams = ByronProtParams; - let mut tx_ins: ByronTxIns = empty_tx_ins(); - let tx_in: ByronTxIn = new_tx_in(rand_tx_id(), 3); - add_byron_tx_in(&mut tx_ins, &tx_in); - let mut tx_outs: ByronTxOuts = new_tx_outs(); - let tx_out_addr: Address = new_addr(rand_addr_payload(), 0); - let tx_out: ByronTxOut = new_tx_out(tx_out_addr, 99091); - add_tx_out(&mut tx_outs, &tx_out); + fn cbor_to_bytes(input: &str) -> Vec { + hex::decode(input).unwrap() + } + + fn mainnet_tx_from_bytes_cbor<'a>(tx_cbor: &'a Vec) -> MintedTxPayload<'a> { + pallas_codec::minicbor::decode::(&tx_cbor[..]).unwrap() + } + + fn build_utxo<'a>(tx: &Tx) -> UTxOs<'a> { + let mut tx_ins: Vec = tx.inputs.clone().to_vec(); + assert_eq!(tx_ins.len(), 1, "Unexpected number of inputs."); + let tx_in: TxIn = tx_ins.pop().unwrap(); + let address_payload = + "83581cff66e7549ee0706abe5ce63ba325f792f2c1145d918baf563db2b457a101581e581cca3e553c9c63\ + c5927480e7434620200eb3a162ef0b6cf6f671ba925100"; + 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."), + }; + let tx_out: TxOut = TxOut { + address: input_tx_out_addr, + amount: 19999000000, + }; let mut utxos: UTxOs = new_utxos(); - // input_tx_out is the ByronTxOut associated with tx_in. - let input_tx_out_addr: Address = new_addr(rand_addr_payload(), 0); - let input_tx_out: ByronTxOut = new_tx_out(input_tx_out_addr, 100000); - add_to_utxo(&mut utxos, tx_in, input_tx_out); - let validation_result = mk_byron_tx_and_validate( - &new_tx(tx_ins, tx_outs, empty_attributes()), - &empty_witnesses(), - &utxos, - &protocol_params, - ); - match validation_result { + add_to_utxo(&mut utxos, tx_in, tx_out); + utxos + } + + #[test] + 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 utxos: UTxOs = build_utxo(&mtxp.transaction); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { Ok(()) => (), Err(err) => assert!(false, "Unexpected error ({:?}).", err), } } #[test] - // Similar to successful_case, except that no inputs are added to the - // transaction, which should raise a ValidationError:TxInsEmpty error. + // Identical to successful_mainnet_tx, except that all inputs are removed. fn empty_ins() { - let protocol_params: ByronProtParams = ByronProtParams; - let tx_ins: ByronTxIns = empty_tx_ins(); - // Note: tx_in is not added to tx_ins, it is only added to the UTxOs set - let tx_in: ByronTxIn = new_tx_in(rand_tx_id(), 3); - let mut tx_outs: ByronTxOuts = new_tx_outs(); - let tx_out_addr: Address = new_addr(rand_addr_payload(), 0); - let tx_out: ByronTxOut = new_tx_out(tx_out_addr, 99091); - add_tx_out(&mut tx_outs, &tx_out); - let mut utxos: UTxOs = new_utxos(); - let input_tx_out_addr: Address = new_addr(rand_addr_payload(), 0); - let input_tx_out: ByronTxOut = new_tx_out(input_tx_out_addr, 100000); - add_to_utxo(&mut utxos, tx_in, input_tx_out); - let validation_result = mk_byron_tx_and_validate( - &new_tx(tx_ins, tx_outs, empty_attributes()), - &empty_witnesses(), - &utxos, - &protocol_params, - ); - match validation_result { + 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 = build_utxo(&mtxp.transaction); + // Clear the set of inputs in the transaction. + let mut tx: Tx = (*mtxp.transaction).clone(); + tx.inputs = MaybeIndefArray::Def(Vec::new()); + let mut tx_buf: Vec = Vec::new(); + match encode(tx, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?}).", err), + }; + mtxp.transaction = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { Ok(()) => assert!(false, "Inputs set should not be empty."), Err(err) => match err { ValidationError::TxInsEmpty => (), @@ -91,25 +102,29 @@ mod byron_tests { } #[test] - // Similar to empty_ins, except that this time no outputs are added to the - // transaction, which should raise a ValidationError:TxOutsEmpty error. + // Identical to successful_mainnet_tx, except that all outputs are removed. fn empty_outs() { - let protocol_params: ByronProtParams = ByronProtParams; - let mut tx_ins: ByronTxIns = empty_tx_ins(); - let tx_in: ByronTxIn = new_tx_in(rand_tx_id(), 3); - add_byron_tx_in(&mut tx_ins, &tx_in); - let tx_outs: ByronTxOuts = new_tx_outs(); - let mut utxos: UTxOs = new_utxos(); - let input_tx_out_addr: Address = new_addr(rand_addr_payload(), 0); - let input_tx_out: ByronTxOut = new_tx_out(input_tx_out_addr, 100000); - add_to_utxo(&mut utxos, tx_in, input_tx_out); - let validation_result = mk_byron_tx_and_validate( - &new_tx(tx_ins, tx_outs, empty_attributes()), - &empty_witnesses(), - &utxos, - &protocol_params, - ); - match validation_result { + 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 = build_utxo(&mtxp.transaction); + // 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) => assert!(false, "Unable to encode Tx ({:?}).", err), + }; + mtxp.transaction = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { Ok(()) => assert!(false, "Outputs set should not be empty."), Err(err) => match err { ValidationError::TxOutsEmpty => (), @@ -119,28 +134,20 @@ mod byron_tests { } #[test] - // The UTxO set does not contain an entry for the single input to this transaction. This - // represents the situation where a transaction tries to spend a non-existent UTxO (e.g., one - // which has already been spent). + // The transaction is valid, but the UTxO set is empty. fn unfound_utxo() { - let protocol_params: ByronProtParams = ByronProtParams; - let mut tx_ins: ByronTxIns = empty_tx_ins(); - let tx_in: ByronTxIn = new_tx_in(rand_tx_id(), 3); - add_byron_tx_in(&mut tx_ins, &tx_in); - let mut tx_outs: ByronTxOuts = new_tx_outs(); - let tx_out_addr: Address = new_addr(rand_addr_payload(), 0); - let tx_out: ByronTxOut = new_tx_out(tx_out_addr, 99091); - add_tx_out(&mut tx_outs, &tx_out); - // Note: utxos is empty, hence the only input to this transaction will not be found, for - // which an error should be raised. - let utxos: UTxOs = new_utxos(); - let validation_result = mk_byron_tx_and_validate( - &new_tx(tx_ins, tx_outs, empty_attributes()), - &empty_witnesses(), - &utxos, - &protocol_params, - ); - match validation_result { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); + let mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let utxos: UTxOs = UTxOs::new(); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { Ok(()) => assert!(false, "All inputs must be within the UTxO set."), Err(err) => match err { ValidationError::InputMissingInUTxO => (), @@ -148,135 +155,206 @@ mod byron_tests { }, } } -} -// Types aliases. -type ByronTxIns = MaybeIndefArray; -type ByronTxOuts = MaybeIndefArray; - -// Helper functions. -fn empty_tx_ins() -> ByronTxIns { - MaybeIndefArray::Def(Vec::new()) -} - -fn rand_tx_id() -> ByronTxId { - let mut rng = rand::thread_rng(); - let mut bytes = [0u8; 32]; - for elem in bytes.iter_mut() { - *elem = rng.gen(); - } - Hash::new(bytes) -} - -fn new_tx_in(tx_id: ByronTxId, index: u32) -> ByronTxIn { - ByronTxIn::Variant0(CborWrap((tx_id, index))) -} - -fn add_byron_tx_in(ins: &mut ByronTxIns, new_in: &ByronTxIn) { - match ins { - MaybeIndefArray::Def(vec) | MaybeIndefArray::Indef(vec) => vec.push(new_in.clone()), + #[test] + // 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 = build_utxo(&mtxp.transaction); + // Remove lovelace from output. + let mut tx: Tx = (*mtxp.transaction).clone(); + let altered_tx_out: TxOut = TxOut { + address: tx.outputs[0].address.clone(), + amount: 0, + }; + let mut new_tx_outs: Vec = Vec::new(); + new_tx_outs.push(tx.outputs[1].clone()); + new_tx_outs.push(altered_tx_out); + tx.outputs = MaybeIndefArray::Indef(new_tx_outs); + let mut tx_buf: Vec = Vec::new(); + match encode(tx, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?}).", err), + }; + mtxp.transaction = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => assert!(false, "All outputs must contain lovelace."), + Err(err) => match err { + ValidationError::OutputWithoutLovelace => (), + _ => assert!(false, "Unexpected error ({:?}).", err), + }, + } } -} - -fn new_tx_outs() -> ByronTxOuts { - MaybeIndefArray::Def(Vec::new()) -} -fn rand_addr_payload() -> TagWrap { - let mut rng = rand::thread_rng(); - let mut bytes = [0u8; 24]; - for elem in bytes.iter_mut() { - *elem = rng.gen(); + #[test] + // 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 utxos: UTxOs = build_utxo(&mtxp.transaction); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 1000, + min_fees_factor: 1000, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => assert!(false, "Fees should not be below minimum."), + Err(err) => match err { + ValidationError::FeesBelowMin => (), + _ => assert!(false, "Unexpected error ({:?}).", err), + }, + } } - TagWrap::::new(ByteVec::from(bytes.to_vec())) -} -fn new_addr(payload: TagWrap, crc: u32) -> Address { - Address { - payload: payload, - crc: crc, + #[test] + // 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 utxos: UTxOs = build_utxo(&mtxp.transaction); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 0, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => assert!(false, "Transaction size cannot exceed protocol limit."), + Err(err) => match err { + ValidationError::MaxTxSizeExceeded => (), + _ => assert!(false, "Unexpected error ({:?}).", err), + }, + } } -} -fn new_tx_out(address: Address, amount: u64) -> ByronTxOut { - ByronTxOut { - address: address, - amount: amount, + #[test] + // 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 = build_utxo(&mtxp.transaction); + // 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) => assert!(false, "Unable to encode Tx ({:?}).", err), + }; + mtxp.witness = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => assert!(false, "All inputs must have a witness signature."), + Err(err) => match err { + ValidationError::MissingWitness => (), + _ => assert!(false, "Unexpected error ({:?}).", err), + }, + } } -} -fn add_tx_out(outs: &mut ByronTxOuts, new_out: &ByronTxOut) { - match outs { - MaybeIndefArray::Def(vec) | MaybeIndefArray::Indef(vec) => vec.push(new_out.clone()), + #[test] + // The input to the transaction has an associated witness, but the signature is 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 = build_utxo(&mtxp.transaction); + // Modify signature in witness + let new_wit: Twit = match mtxp.witness[0].clone() { + Twit::PkWitness(CborWrap((pk, _))) => { + Twit::PkWitness(CborWrap((pk, [0u8; 64].to_vec().into()))) + } + _ => unreachable!(), + }; + let mut new_witnesses_vec = Vec::new(); + new_witnesses_vec.push(new_wit); + let new_witnesses: Witnesses = MaybeIndefArray::Def(new_witnesses_vec); + let mut tx_buf: Vec = Vec::new(); + match encode(new_witnesses, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?}).", err), + }; + mtxp.witness = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => assert!(false, "Witness signature should verify the transaction."), + Err(err) => match err { + ValidationError::WrongSignature => (), + _ => assert!(false, "Unexpected error ({:?}).", err), + }, + } } } -fn add_to_utxo<'a>(utxos: &mut UTxOs<'a>, tx_in: ByronTxIn, tx_out: ByronTxOut) { +// 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); } -fn empty_attributes() -> Attributes { - EmptyMap -} - -// pallas_applying::validate takes a MultiEraTx, not a ByronTx and a -// ByronWitnesses. To be able to build a MultiEraTx from a ByronTx and a -// ByronWitnesses, we need to encode each of them and then decode them into -// KeepRaw and KeepRaw values, respectively. +// 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( - btx: &ByronTx, - bwit: &ByronWitnesses, + tx: &Tx, + wits: &Witnesses, utxos: &UTxOs, - prot_pps: &ByronProtParams, + env: &Environment, ) -> ValidationResult { - // Encode btx and decode into a KeepRaw value. - let mut btx_buf: Vec = Vec::new(); - match encode(btx, &mut btx_buf) { + let mut tx_buf: Vec = Vec::new(); + match encode(tx, &mut tx_buf) { Ok(_) => (), - Err(err) => assert!(false, "Unable to encode ByronTx ({:?}).", err), + Err(err) => assert!(false, "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 kpbtx: KeepRaw = - match Decode::decode(&mut Decoder::new(&btx_buf.as_slice()), &mut ()) { - Ok(kp) => kp, - Err(err) => panic!("Unable to decode ByronTx ({:?}).", err), - }; - // Encode bwit and decode into a KeepRaw value. let mut wit_buf: Vec = Vec::new(); - match encode(bwit, &mut wit_buf) { + match encode(wits, &mut wit_buf) { Ok(_) => (), - Err(err) => assert!(false, "Unable to encode ByronWitnesses ({:?}).", err), + Err(err) => assert!(false, "Unable to encode Witnesses ({:?}).", err), }; - let kpbwit: KeepRaw = + let kpwit: KeepRaw = match Decode::decode(&mut Decoder::new(&wit_buf.as_slice()), &mut ()) { Ok(kp) => kp, - Err(err) => panic!("Unable to decode ByronWitnesses ({:?}).", err), + Err(err) => panic!("Unable to decode Witnesses ({:?}).", err), }; - let mtxp: ByronTxPayload = ByronTxPayload { - transaction: kpbtx, - witness: kpbwit, + let mtxp: MintedTxPayload = MintedTxPayload { + transaction: kptx, + witness: kpwit, }; let metx: MultiEraTx = MultiEraTx::from_byron(&mtxp); - validate( - &metx, - utxos, - &MultiEraProtParams::Byron(Box::new(Cow::Borrowed(&prot_pps))), - ) -} - -fn new_tx(ins: ByronTxIns, outs: ByronTxOuts, attrs: Attributes) -> ByronTx { - ByronTx { - inputs: ins, - outputs: outs, - attributes: attrs, - } -} - -fn empty_witnesses() -> ByronWitnesses { - MaybeIndefArray::Def(Vec::new()) + validate(&metx, utxos, env) } fn new_utxos<'a>() -> UTxOs<'a> { diff --git a/test_data/byron1.tx b/test_data/byron1.tx new file mode 100644 index 00000000..d5ec0a9e --- /dev/null +++ b/test_data/byron1.tx @@ -0,0 +1 @@ +82839f8200d8185824825820da832fb5ef57df5b91817e9a7448d26e92552afb34f8ee5adb491b24bbe990d50eff9f8282d818584283581cdac5d9464c2140aeb0e3b6d69f0657e61f51e0c259fe19681ed268e8a101581e581c2b5a44277e3543c08eae5d9d9d1146f43ba009fea6e285334f2549be001ae69c4d201b0000000172a84e408282d818584283581c2b8e5e0cb6495ec275872d1340b0581613b04a49a3c6f2f760ecaf95a101581e581cca3e553c9c63c5b66689e943ce7dad7d560ae84d7c2eaf21611c024c001ad27c159a1b00000003355d95efffa0818200d8185885825840888cdf85991d85f2023423ba4c80d41570ebf1fc878c9f5731df1d20c64aecf3e8aa2bbafc9beba8ef33acb4d7e199b445229085718fba83b7f86ab6a3bcf782584063e34cf5fa6d8c0288630437fa5e151d93907e826e66ba273145e3ee712930b6f446ff81cb91d7f0cb4ceccd0466ba9ab14448d7eab9fc480a122324bd80170e \ No newline at end of file