From 986ee75761c46aa02bd39be28e1c01fb3a4f86d5 Mon Sep 17 00:00:00 2001 From: Maico Leberle Date: Tue, 10 Oct 2023 17:16:26 -0300 Subject: [PATCH] feat: scaffold Byron phase-1 validations (#300) Co-authored-by: Santiago Carmuega --- Cargo.toml | 1 + pallas-applying/Cargo.toml | 20 ++ pallas-applying/README.md | 3 + pallas-applying/src/byron.rs | 14 ++ pallas-applying/src/lib.rs | 24 +++ pallas-applying/src/types.rs | 27 +++ pallas-applying/tests/byron.rs | 192 ++++++++++++++++++ pallas-codec/src/utils.rs | 6 +- .../miniprotocols/localtxsubmission/codec.rs | 16 +- pallas-primitives/src/alonzo/model.rs | 6 +- pallas-primitives/src/byron/model.rs | 8 +- pallas-traverse/src/lib.rs | 5 +- pallas/Cargo.toml | 1 + pallas/src/lib.rs | 2 + 14 files changed, 306 insertions(+), 19 deletions(-) create mode 100644 pallas-applying/Cargo.toml create mode 100644 pallas-applying/README.md create mode 100644 pallas-applying/src/byron.rs create mode 100644 pallas-applying/src/lib.rs create mode 100644 pallas-applying/src/types.rs create mode 100644 pallas-applying/tests/byron.rs diff --git a/Cargo.toml b/Cargo.toml index 2fb20d6..03b7c37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "pallas-applying", "pallas-codec", "pallas-addresses", "pallas-network", diff --git a/pallas-applying/Cargo.toml b/pallas-applying/Cargo.toml new file mode 100644 index 0000000..78e6335 --- /dev/null +++ b/pallas-applying/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "pallas-applying" +description = "Logic for validating and applying new blocks and txs to the chain state" +version = "0.19.1" +edition = "2021" +repository = "https://github.com/MaicoLeberle/pallas" +homepage = "https://github.com/MaicoLeberle/pallas" +license = "Apache-2.0" +readme = "README.md" +authors = ["Maico Leberle "] + +[lib] +doctest = false + +[dependencies] +pallas-codec = { path = "../pallas-codec" } +pallas-crypto = { path = "../pallas-crypto" } +pallas-primitives = { path = "../pallas-primitives" } +pallas-traverse = { path = "../pallas-traverse" } +rand = "0.8" diff --git a/pallas-applying/README.md b/pallas-applying/README.md new file mode 100644 index 0000000..e1d43db --- /dev/null +++ b/pallas-applying/README.md @@ -0,0 +1,3 @@ +# Pallas Applying + +Crate for performing transaction validation according to the Cardano protocol. diff --git a/pallas-applying/src/byron.rs b/pallas-applying/src/byron.rs new file mode 100644 index 0000000..1b646ef --- /dev/null +++ b/pallas-applying/src/byron.rs @@ -0,0 +1,14 @@ +//! Utilities required for Byron-era transaction validation. + +use crate::types::{ByronProtParams, UTxOs, ValidationResult}; + +use pallas_primitives::byron::MintedTxPayload; + +// TODO: implement each of the validation rules. +pub fn validate_byron_tx( + _mtxp: &MintedTxPayload, + _utxos: &UTxOs, + _prot_pps: &ByronProtParams, +) -> ValidationResult { + Ok(()) +} diff --git a/pallas-applying/src/lib.rs b/pallas-applying/src/lib.rs new file mode 100644 index 0000000..b1f00c6 --- /dev/null +++ b/pallas-applying/src/lib.rs @@ -0,0 +1,24 @@ +//! Logic for validating and applying new blocks and txs to the chain state + +pub mod byron; +pub mod types; + +use byron::validate_byron_tx; + +use pallas_traverse::{MultiEraTx, MultiEraTx::Byron as ByronTxPayload}; + +pub use types::{ + MultiEraProtParams, MultiEraProtParams::Byron as ByronProtParams, 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), + // TODO: implement the rest of the eras. + _ => Ok(()), + } +} diff --git a/pallas-applying/src/types.rs b/pallas-applying/src/types.rs new file mode 100644 index 0000000..52eb041 --- /dev/null +++ b/pallas-applying/src/types.rs @@ -0,0 +1,27 @@ +//! Base types used for validating transactions in each era. + +use std::{borrow::Cow, 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; + +// TODO: add variants for the other eras. +#[derive(Debug)] +#[non_exhaustive] +pub enum MultiEraProtParams<'b> { + Byron(Box>), +} + +// TODO: replace this generic variant with validation-rule-specific ones. +#[derive(Debug)] +#[non_exhaustive] +pub enum ValidationError { + ValidationError, +} + +pub type ValidationResult = Result<(), ValidationError>; diff --git a/pallas-applying/tests/byron.rs b/pallas-applying/tests/byron.rs new file mode 100644 index 0000000..d41b54d --- /dev/null +++ b/pallas-applying/tests/byron.rs @@ -0,0 +1,192 @@ +use rand::Rng; +use std::{borrow::Cow, vec::Vec}; + +use pallas_applying::{ + types::{ByronProtParams, MultiEraProtParams}, + validate, UTxOs, ValidationResult, +}; +use pallas_codec::{ + minicbor::{ + bytes::ByteVec, + 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, +}; +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_byron_tx_ins(); + let tx_in: ByronTxIn = new_tx_in(random_tx_id(), 3); + add_byron_tx_in(&mut tx_ins, &tx_in); + let mut tx_outs: ByronTxOuts = new_byron_tx_outs(); + let tx_out: ByronTxOut = new_byron_tx_out(new_address(random_address_payload(), 0), 99091); + add_byron_tx_out(&mut tx_outs, &tx_out); + let mut utxos: UTxOs = new_utxos(); + // input_tx_out is the ByronTxOut associated with tx_in. + let input_tx_out: ByronTxOut = + new_byron_tx_out(new_address(random_address_payload(), 0), 100000); + add_to_utxo(&mut utxos, tx_in, input_tx_out); + let validation_result = mk_byron_tx_and_validate( + &new_byron_tx(tx_ins, tx_outs, empty_byron_attributes()), + &empty_byron_witnesses(), + &utxos, + &protocol_params, + ); + match validation_result { + Ok(()) => (), + Err(err) => assert!(false, "Unexpected error (sucessful_case - {:?}).", err), + } + } +} + +// Types aliases. +type ByronTxIns = MaybeIndefArray; +type ByronTxOuts = MaybeIndefArray; + +// Helper functions. +fn empty_byron_tx_ins() -> ByronTxIns { + MaybeIndefArray::Def(Vec::new()) +} + +fn random_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()), + } +} + +fn new_byron_tx_outs() -> ByronTxOuts { + MaybeIndefArray::Def(Vec::new()) +} + +fn random_address_payload() -> TagWrap { + let mut rng = rand::thread_rng(); + let mut bytes = [0u8; 24]; + for elem in bytes.iter_mut() { + *elem = rng.gen(); + } + TagWrap::::new(ByteVec::from(bytes.to_vec())) +} + +fn new_address(payload: TagWrap, crc: u32) -> Address { + Address { + payload: payload, + crc: crc, + } +} + +fn new_byron_tx_out(address: Address, amount: u64) -> ByronTxOut { + ByronTxOut { + address: address, + amount: amount, + } +} + +fn add_byron_tx_out(outs: &mut ByronTxOuts, new_out: &ByronTxOut) { + match outs { + MaybeIndefArray::Def(vec) | MaybeIndefArray::Indef(vec) => vec.push(new_out.clone()), + } +} + +fn add_to_utxo<'a>(utxos: &mut UTxOs<'a>, tx_in: ByronTxIn, tx_out: ByronTxOut) { + 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_byron_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. +fn mk_byron_tx_and_validate( + btx: &ByronTx, + bwit: &ByronWitnesses, + utxos: &UTxOs, + prot_pps: &ByronProtParams, +) -> ValidationResult { + // Encode btx and decode into a KeepRaw value. + let mut btx_buf: Vec = Vec::new(); + match encode(btx, &mut btx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode ByronTx ({:?}).", 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) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode ByronWitnesses ({:?}).", err), + }; + let kpbwit: KeepRaw = + match Decode::decode(&mut Decoder::new(&wit_buf.as_slice()), &mut ()) { + Ok(kp) => kp, + Err(err) => panic!("Unable to decode ByronWitnesses ({:?}).", err), + }; + + let mtxp: ByronTxPayload = ByronTxPayload { + transaction: kpbtx, + witness: kpbwit, + }; + let metx: MultiEraTx = MultiEraTx::from_byron(&mtxp); + validate( + &metx, + utxos, + &MultiEraProtParams::Byron(Box::new(Cow::Borrowed(&prot_pps))), + ) +} + +fn new_byron_tx(ins: ByronTxIns, outs: ByronTxOuts, attrs: Attributes) -> ByronTx { + ByronTx { + inputs: ins, + outputs: outs, + attributes: attrs, + } +} + +fn empty_byron_witnesses() -> ByronWitnesses { + MaybeIndefArray::Def(Vec::new()) +} + +fn new_utxos<'a>() -> UTxOs<'a> { + UTxOs::new() +} diff --git a/pallas-codec/src/utils.rs b/pallas-codec/src/utils.rs index 4b1c9dd..5f088dd 100644 --- a/pallas-codec/src/utils.rs +++ b/pallas-codec/src/utils.rs @@ -1,6 +1,6 @@ use minicbor::{data::Tag, Decode, Encode}; use serde::{Deserialize, Serialize}; -use std::{fmt, ops::Deref}; +use std::{fmt, hash::Hash as StdHash, ops::Deref}; /// Utility for skipping parts of the CBOR payload, use only for debugging #[derive(Debug, PartialEq, PartialOrd, Eq, Ord)] @@ -292,7 +292,7 @@ where } /// Wraps a struct so that it is encoded/decoded as a cbor bytes -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, StdHash)] #[serde(transparent)] pub struct CborWrap(pub T); @@ -390,7 +390,7 @@ impl Deref for TagWrap { /// An empty map /// /// don't ask me why, that's what the CDDL asks for. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct EmptyMap; impl<'b, C> minicbor::decode::Decode<'b, C> for EmptyMap { diff --git a/pallas-network/src/miniprotocols/localtxsubmission/codec.rs b/pallas-network/src/miniprotocols/localtxsubmission/codec.rs index c05d264..ce54a8a 100644 --- a/pallas-network/src/miniprotocols/localtxsubmission/codec.rs +++ b/pallas-network/src/miniprotocols/localtxsubmission/codec.rs @@ -1,12 +1,12 @@ -use pallas_codec::minicbor::{decode, Decode, Decoder, encode, Encode, Encoder}; use pallas_codec::minicbor::data::Tag; +use pallas_codec::minicbor::{decode, encode, Decode, Decoder, Encode, Encoder}; use crate::miniprotocols::localtxsubmission::{EraTx, Message, RejectReason}; impl Encode<()> for Message - where - Tx: Encode<()>, - Reject: Encode<()>, +where + Tx: Encode<()>, + Reject: Encode<()>, { fn encode( &self, @@ -97,14 +97,14 @@ impl Encode<()> for RejectReason { ) -> Result<(), encode::Error> { e.writer_mut() .write_all(&self.0) - .map_err(|w_err| encode::Error::write(w_err))?; + .map_err(encode::Error::write)?; Ok(()) } } #[cfg(test)] mod tests { - use pallas_codec::{Fragment, minicbor}; + use pallas_codec::{minicbor, Fragment}; use crate::miniprotocols::localtxsubmission::{EraTx, Message, RejectReason}; use crate::multiplexer::Error; @@ -117,8 +117,8 @@ mod tests { } fn try_decode_message(buffer: &mut Vec) -> Result, Error> - where - M: Fragment, + where + M: Fragment, { let mut decoder = minicbor::Decoder::new(buffer); let maybe_msg = decoder.decode(); diff --git a/pallas-primitives/src/alonzo/model.rs b/pallas-primitives/src/alonzo/model.rs index ad17600..efc08a2 100644 --- a/pallas-primitives/src/alonzo/model.rs +++ b/pallas-primitives/src/alonzo/model.rs @@ -3,7 +3,7 @@ //! Handcrafted, idiomatic rust artifacts based on based on the [Alonzo CDDL](https://github.com/input-output-hk/cardano-ledger/blob/master/eras/alonzo/test-suite/cddl-files/alonzo.cddl) file in IOHK repo. use serde::{Deserialize, Serialize}; -use std::{fmt, ops::Deref}; +use std::{fmt, hash::Hash as StdHash, ops::Deref}; use pallas_codec::minicbor::{data::Tag, Decode, Encode}; use pallas_crypto::hash::Hash; @@ -78,7 +78,9 @@ pub struct Header { pub body_signature: Bytes, } -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +#[derive( + Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, StdHash, +)] pub struct TransactionInput { #[n(0)] pub transaction_id: Hash<32>, diff --git a/pallas-primitives/src/byron/model.rs b/pallas-primitives/src/byron/model.rs index 59a69f3..f5e8cdf 100644 --- a/pallas-primitives/src/byron/model.rs +++ b/pallas-primitives/src/byron/model.rs @@ -12,6 +12,8 @@ use pallas_codec::utils::{ // required for derive attrs to work use pallas_codec::minicbor; +use std::hash::Hash as StdHash; + // Basic Cardano Types pub type Blake2b256 = Hash<32>; @@ -64,7 +66,7 @@ pub struct Address { // Transactions // txout = [address, u64] -#[derive(Debug, Encode, Decode, Clone)] +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq)] pub struct TxOut { #[n(0)] pub address: Address, @@ -73,7 +75,7 @@ pub struct TxOut { pub amount: u64, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, StdHash)] pub enum TxIn { // [0, #6.24(bytes .cbor ([txid, u32]))] Variant0(CborWrap<(TxId, u32)>), @@ -121,7 +123,7 @@ impl minicbor::Encode for TxIn { } // tx = [[+ txin], [+ txout], attributes] -#[derive(Debug, Encode, Decode, Clone)] +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq)] pub struct Tx { #[n(0)] pub inputs: MaybeIndefArray, diff --git a/pallas-traverse/src/lib.rs b/pallas-traverse/src/lib.rs index ec646a9..692f847 100644 --- a/pallas-traverse/src/lib.rs +++ b/pallas-traverse/src/lib.rs @@ -1,7 +1,6 @@ //! Utilities to traverse over multi-era block data -use std::borrow::Cow; -use std::fmt::Display; +use std::{borrow::Cow, fmt::Display, hash::Hash as StdHash}; use thiserror::Error; @@ -89,7 +88,7 @@ pub enum MultiEraOutput<'b> { Byron(Box>), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, StdHash)] #[non_exhaustive] pub enum MultiEraInput<'b> { Byron(Box>), diff --git a/pallas/Cargo.toml b/pallas/Cargo.toml index 3ed3d8f..30d4e3f 100644 --- a/pallas/Cargo.toml +++ b/pallas/Cargo.toml @@ -11,6 +11,7 @@ readme = "../README.md" authors = ["Santiago Carmuega "] [dependencies] +pallas-applying = { version = "=0.19.1", path = "../pallas-applying/" } pallas-network = { version = "=0.19.1", path = "../pallas-network/" } pallas-primitives = { version = "=0.19.1", path = "../pallas-primitives/" } pallas-traverse = { version = "=0.19.1", path = "../pallas-traverse/" } diff --git a/pallas/src/lib.rs b/pallas/src/lib.rs index 901b1d7..878399f 100644 --- a/pallas/src/lib.rs +++ b/pallas/src/lib.rs @@ -40,3 +40,5 @@ pub mod interop { #[doc(inline)] pub use pallas_utxorpc as utxorpc; } + +pub use pallas_applying as applying;