From d43f540367b478666bf1503b6bd65fd4637d8a79 Mon Sep 17 00:00:00 2001 From: Maico Date: Thu, 16 Nov 2023 13:38:26 -0300 Subject: [PATCH] feat(applying): check all inputs in UTxO set --- pallas-applying/src/shelley.rs | 16 ++++-- pallas-applying/tests/byron.rs | 6 +-- pallas-applying/tests/shelley.rs | 85 +++++++++++++++++++++++++++++--- test_data/shelley1.address | 1 + 4 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 test_data/shelley1.address diff --git a/pallas-applying/src/shelley.rs b/pallas-applying/src/shelley.rs index 73d4b3901..a4c6b120c 100644 --- a/pallas-applying/src/shelley.rs +++ b/pallas-applying/src/shelley.rs @@ -1,18 +1,19 @@ //! Utilities required for Shelley-era transaction validation. use crate::types::{ShelleyProtParams, UTxOs, ValidationError, ValidationResult}; - use pallas_primitives::alonzo::{MintedTx, TransactionBody}; +use pallas_traverse::MultiEraInput; // TODO: implement each of the validation rules. pub fn validate_shelley_tx( mtx: &MintedTx, - _utxos: &UTxOs, + utxos: &UTxOs, _prot_pps: &ShelleyProtParams, _prot_magic: &u32, ) -> ValidationResult { let tx_body: &TransactionBody = &mtx.transaction_body; - check_ins_not_empty(tx_body) + check_ins_not_empty(tx_body)?; + check_ins_in_utxos(tx_body, utxos) } fn check_ins_not_empty(tx_body: &TransactionBody) -> ValidationResult { @@ -21,3 +22,12 @@ fn check_ins_not_empty(tx_body: &TransactionBody) -> ValidationResult { } 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(ValidationError::InputMissingInUTxO); + } + } + Ok(()) +} diff --git a/pallas-applying/tests/byron.rs b/pallas-applying/tests/byron.rs index 37b0078f0..69de1669e 100644 --- a/pallas-applying/tests/byron.rs +++ b/pallas-applying/tests/byron.rs @@ -49,6 +49,7 @@ mod byron_tests { } #[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 = tx_from_cbor(&cbor_bytes); @@ -56,9 +57,7 @@ mod byron_tests { 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 { @@ -75,6 +74,7 @@ mod byron_tests { } #[test] + // Transaction hash: a06e5a0150e09f8983be2deafab9e04afc60d92e7110999eb672c903343f1e26 fn successful_mainnet_tx() { let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); let mtxp: MintedTxPayload = tx_from_cbor(&cbor_bytes); diff --git a/pallas-applying/tests/shelley.rs b/pallas-applying/tests/shelley.rs index 5fae102b6..86cbe9358 100644 --- a/pallas-applying/tests/shelley.rs +++ b/pallas-applying/tests/shelley.rs @@ -1,13 +1,21 @@ +use std::borrow::Cow; + use pallas_applying::{ types::{Environment, MultiEraProtParams, ShelleyProtParams, ValidationError}, validate, UTxOs, }; -use pallas_codec::minicbor::{ - decode::{Decode, Decoder}, - encode, +use pallas_codec::{ + minicbor::{ + decode::{Decode, Decoder}, + encode, + }, + utils::Bytes, +}; +use pallas_crypto::hash::Hash; +use pallas_primitives::alonzo::{ + MintedTx, TransactionBody, TransactionInput, TransactionOutput, Value, }; -use pallas_primitives::alonzo::{MintedTx, TransactionBody}; -use pallas_traverse::{Era, MultiEraTx}; +use pallas_traverse::{Era, MultiEraInput, MultiEraOutput, MultiEraTx}; #[cfg(test)] mod byron_tests { @@ -21,7 +29,32 @@ mod byron_tests { 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_tx() { let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/shelley1.tx")); let mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); @@ -30,7 +63,12 @@ mod byron_tests { prot_params: MultiEraProtParams::Shelley(ShelleyProtParams), prot_magic: 764824073, }; - let utxos: UTxOs = UTxOs::new(); + 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(()) => (), Err(err) => assert!(false, "Unexpected error ({:?}).", err), @@ -42,6 +80,12 @@ mod byron_tests { 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(); @@ -57,7 +101,6 @@ mod byron_tests { prot_params: MultiEraProtParams::Shelley(ShelleyProtParams), prot_magic: 764824073, }; - let utxos: UTxOs = UTxOs::new(); match validate(&metx, &utxos, &env) { Ok(()) => assert!(false, "Inputs set should not be empty."), Err(err) => match err { @@ -66,4 +109,32 @@ mod byron_tests { }, } } + + #[test] + // The transaction is valid, but 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 env: Environment = Environment { + prot_params: MultiEraProtParams::Shelley(ShelleyProtParams), + prot_magic: 764824073, + }; + let utxos: UTxOs = UTxOs::new(); + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "All inputs must be within the UTxO set."), + Err(err) => match err { + ValidationError::InputMissingInUTxO => (), + _ => 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/shelley1.address b/test_data/shelley1.address new file mode 100644 index 000000000..125a934c3 --- /dev/null +++ b/test_data/shelley1.address @@ -0,0 +1 @@ +0129bb156d52d014bb444a14138cbee36044c6faed37d0c2d49d2358315c465cbf8c5536970e8a29bb7adcda0d663b20007d481813694c64ef \ No newline at end of file