diff --git a/pallas-codec/src/utils.rs b/pallas-codec/src/utils.rs index 46fadb0b..9f90d772 100644 --- a/pallas-codec/src/utils.rs +++ b/pallas-codec/src/utils.rs @@ -386,6 +386,16 @@ impl MaybeIndefArray { pub fn to_vec(self) -> Vec { self.into() } + + pub fn map_into(self, f: F) -> MaybeIndefArray + where + F: FnMut(A) -> B, + { + match self { + MaybeIndefArray::Def(x) => MaybeIndefArray::Def(x.into_iter().map(f).collect()), + MaybeIndefArray::Indef(x) => MaybeIndefArray::Indef(x.into_iter().map(f).collect()), + } + } } impl Deref for MaybeIndefArray { @@ -1157,6 +1167,71 @@ impl minicbor::Encode for KeepRaw<'_, T> { } } +/// Decodes just a raw bytes with skipping actual decoding of the CBOR object. +/// Stores the original CBOR bytes for further decoding. +/// +/// # Examples +/// +/// ``` +/// use pallas_codec::utils::OnlyRaw; +/// +/// let a = (123u16, (456u16, 789u16), 123u16); +/// let data = minicbor::to_vec(a).unwrap(); +/// +/// let (_, keeper, _): (u16, OnlyRaw<(u16, u16)>, u16) = minicbor::decode(&data).unwrap(); +/// let confirm: (u16, u16) = keeper.decode().unwrap(); +/// assert_eq!(confirm, (456u16, 789u16)); +/// ``` +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct OnlyRaw<'b, T> { + raw: &'b [u8], + _p: std::marker::PhantomData, +} + +impl<'b, T> OnlyRaw<'b, T> { + pub fn raw_cbor(&self) -> &'b [u8] { + self.raw + } +} + +impl<'b, T> OnlyRaw<'b, T> +where + T: minicbor::Decode<'b, ()>, +{ + pub fn decode(&self) -> Result { + minicbor::decode(self.raw) + } +} + +impl<'b, T, C> minicbor::Decode<'b, C> for OnlyRaw<'b, T> +where + T: minicbor::Decode<'b, C>, +{ + fn decode(d: &mut minicbor::Decoder<'b>, _: &mut C) -> Result { + let all = d.input(); + let start = d.position(); + d.skip()?; + let end = d.position(); + + Ok(Self { + raw: &all[start..end], + _p: std::marker::PhantomData, + }) + } +} + +impl minicbor::Encode for OnlyRaw<'_, T> { + fn encode( + &self, + e: &mut minicbor::Encoder, + _ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.writer_mut() + .write_all(self.raw_cbor()) + .map_err(minicbor::encode::Error::write) + } +} + /// Struct to hold arbitrary CBOR to be processed independently /// /// # Examples diff --git a/pallas-primitives/Cargo.toml b/pallas-primitives/Cargo.toml index 30dc0d3c..706751dc 100644 --- a/pallas-primitives/Cargo.toml +++ b/pallas-primitives/Cargo.toml @@ -13,6 +13,10 @@ authors = [ "Lucas Rosa ", ] +[[bench]] +name = "alonzo_decoding" +harness = false + [dependencies] hex = "0.4.3" log = "0.4.14" @@ -23,6 +27,9 @@ bech32 = "0.9.0" serde = { version = "1.0.136", optional = true, features = ["derive"] } serde_json = { version = "1.0.79", optional = true } +[dev-dependencies] +criterion = { version = "0.5.1" } + [features] json = ["serde", "serde_json"] default = ["json"] diff --git a/pallas-primitives/benches/alonzo_decoding.rs b/pallas-primitives/benches/alonzo_decoding.rs new file mode 100644 index 00000000..6b9641a5 --- /dev/null +++ b/pallas-primitives/benches/alonzo_decoding.rs @@ -0,0 +1,30 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use pallas_codec::utils::{KeepRaw, OnlyRaw}; +use pallas_primitives::alonzo::AuxiliaryData; + +const AUXILARY_HEX: &'static str = "d90103a100a11902d1a278386238663665376634326563336264303239363662343034313131616530653233636238356539303236343462666237636539343036303766a1781c63756d44796e616d6963496e746567726174696f6e456e67696e6565a76538316e6f6e6868617264776172656b383973696d696c697175656d41646d696e6973747261746f726b6465736372697074696f6e783e4d79206e65696768626f7220416c69646120686173206f6e65206f662074686573652e2053686520776f726b7320617320612067616d626c657220616e646566696c657383a3696d656469615479706569696d6167652f706e67646e616d65781c63756d44796e616d6963496e746567726174696f6e456e67696e6565637372637835697066733a2f2f516d5a46373437504659565a6d5161363163777351555268586131783634344b415631377778766b454238756577a3696d656469615479706569696d6167652f706e67646e616d65781c63756d44796e616d6963496e746567726174696f6e456e67696e6565637372637835697066733a2f2f516d644633466b59395277686d74783636534e773373637555756f444852745439684e4d36646f63553767326544a3696d656469615479706569696d6167652f706e67646e616d65781c63756d44796e616d6963496e746567726174696f6e456e67696e6565637372637835697066733a2f2f516d62614a75504e336a463857535544667659687a75324b5a716d6a517868506135736d666245445363366a695765696d6167657835697066733a2f2f516d6156524a7a343652384e72476b4a58684c6170316145323642676a365367746564393442734a487576437556696d656469615479706569696d6167652f706e67646e616d65781c63756d44796e616d6963496e746567726174696f6e456e67696e65656776657273696f6e63312e30"; + +fn auxilary_data_benches(c: &mut Criterion) { + let bytes = hex::decode(AUXILARY_HEX).unwrap(); + + let mut group = c.benchmark_group("Alonzo Auxilary Data Decoding"); + group.bench_function("KeepRaw", |b| { + b.iter(|| { + let _aux: KeepRaw = + pallas_codec::minicbor::decode(bytes.as_slice()).unwrap(); + }); + }); + + group.bench_function("OnlyRaw", |b| { + b.iter(|| { + let _aux: OnlyRaw = + pallas_codec::minicbor::decode(bytes.as_slice()).unwrap(); + }); + }); + + group.finish(); +} + +criterion_group!(benches, auxilary_data_benches); + +criterion_main!(benches); \ No newline at end of file diff --git a/pallas-primitives/src/alonzo/model.rs b/pallas-primitives/src/alonzo/model.rs index ba47f0d1..8cabfc28 100644 --- a/pallas-primitives/src/alonzo/model.rs +++ b/pallas-primitives/src/alonzo/model.rs @@ -10,9 +10,10 @@ pub use crate::{ plutus_data::*, AddrKeyhash, AssetName, Bytes, Coin, CostModel, DatumHash, DnsName, Epoch, ExUnitPrices, ExUnits, GenesisDelegateHash, Genesishash, Hash, IPv4, IPv6, Int, KeepRaw, KeyValuePairs, MaybeIndefArray, Metadata, Metadatum, MetadatumLabel, NetworkId, Nonce, - NonceVariant, Nullable, PlutusScript, PolicyId, PoolKeyhash, PoolMetadata, PoolMetadataHash, - Port, PositiveInterval, ProtocolVersion, RationalNumber, Relay, RewardAccount, ScriptHash, - StakeCredential, TransactionIndex, TransactionInput, UnitInterval, VrfCert, VrfKeyhash, + NonceVariant, Nullable, OnlyRaw, PlutusScript, PolicyId, PoolKeyhash, PoolMetadata, + PoolMetadataHash, Port, PositiveInterval, ProtocolVersion, RationalNumber, Relay, + RewardAccount, ScriptHash, StakeCredential, TransactionIndex, TransactionInput, UnitInterval, + VrfCert, VrfKeyhash, }; #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] @@ -823,63 +824,58 @@ impl minicbor::Encode for AuxiliaryData { } #[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)] -pub struct Block { +pub struct PseudoBlock +where + T1: std::clone::Clone, + T2: std::clone::Clone, + T3: std::clone::Clone, + T4: std::clone::Clone, +{ #[n(0)] - pub header: Header, + pub header: T1, #[b(1)] - pub transaction_bodies: Vec, + pub transaction_bodies: MaybeIndefArray, #[n(2)] - pub transaction_witness_sets: Vec, + pub transaction_witness_sets: MaybeIndefArray, #[n(3)] - pub auxiliary_data_set: KeyValuePairs, + pub auxiliary_data_set: KeyValuePairs, #[n(4)] - pub invalid_transactions: Option>, + pub invalid_transactions: Option>, } +pub type Block = PseudoBlock; + /// A memory representation of an already minted block /// /// This structure is analogous to [Block], but it allows to retrieve the /// original CBOR bytes for each structure that might require hashing. In this /// way, we make sure that the resulting hash matches what exists on-chain. -#[derive(Encode, Decode, Debug, PartialEq, Clone)] -pub struct MintedBlock<'b> { - #[n(0)] - pub header: KeepRaw<'b, MintedHeader<'b>>, - - #[b(1)] - pub transaction_bodies: MaybeIndefArray>, - - #[n(2)] - pub transaction_witness_sets: MaybeIndefArray>>, - - #[n(3)] - pub auxiliary_data_set: KeyValuePairs>, - - #[n(4)] - pub invalid_transactions: Option>, -} +pub type MintedBlock<'b> = PseudoBlock< + KeepRaw<'b, MintedHeader<'b>>, + KeepRaw<'b, TransactionBody>, + KeepRaw<'b, MintedWitnessSet<'b>>, + KeepRaw<'b, AuxiliaryData>, +>; + +pub type MintedBlockWithRawAuxiliary<'b> = PseudoBlock< + KeepRaw<'b, MintedHeader<'b>>, + KeepRaw<'b, TransactionBody>, + KeepRaw<'b, MintedWitnessSet<'b>>, + OnlyRaw<'b, AuxiliaryData>, +>; impl<'b> From> for Block { fn from(x: MintedBlock<'b>) -> Self { Block { header: x.header.unwrap().into(), - transaction_bodies: x - .transaction_bodies - .to_vec() - .into_iter() - .map(|x| x.unwrap()) - .collect(), + transaction_bodies: x.transaction_bodies.map_into(|x| x.unwrap()), transaction_witness_sets: x .transaction_witness_sets - .to_vec() - .into_iter() - .map(|x| x.unwrap()) - .map(WitnessSet::from) - .collect(), + .map_into(|x| WitnessSet::from(x.unwrap())), auxiliary_data_set: x .auxiliary_data_set .to_vec() @@ -892,35 +888,39 @@ impl<'b> From> for Block { } } -#[derive(Serialize, Deserialize, Encode, Decode, Debug)] -pub struct Tx { - #[n(0)] - pub transaction_body: TransactionBody, +#[derive(Serialize, Deserialize, Encode, Decode, Debug, Clone)] +pub struct PseudoTx +where + T1: std::clone::Clone, + T2: std::clone::Clone, + T3: std::clone::Clone, +{ + #[b(0)] + pub transaction_body: T1, #[n(1)] - pub transaction_witness_set: WitnessSet, + pub transaction_witness_set: T2, #[n(2)] pub success: bool, #[n(3)] - pub auxiliary_data: Nullable, + pub auxiliary_data: Nullable, } -#[derive(Encode, Decode, Debug, Clone)] -pub struct MintedTx<'b> { - #[b(0)] - pub transaction_body: KeepRaw<'b, TransactionBody>, - - #[n(1)] - pub transaction_witness_set: KeepRaw<'b, MintedWitnessSet<'b>>, +pub type Tx = PseudoTx; - #[n(2)] - pub success: bool, +pub type MintedTx<'b> = PseudoTx< + KeepRaw<'b, TransactionBody>, + KeepRaw<'b, MintedWitnessSet<'b>>, + KeepRaw<'b, AuxiliaryData>, +>; - #[n(3)] - pub auxiliary_data: Nullable>, -} +pub type MintedTxWithRawAuxiliary<'b> = PseudoTx< + KeepRaw<'b, TransactionBody>, + KeepRaw<'b, MintedWitnessSet<'b>>, + OnlyRaw<'b, AuxiliaryData>, +>; #[cfg(test)] mod tests { @@ -928,59 +928,75 @@ mod tests { use crate::{alonzo::PlutusData, Fragment}; - use super::{Header, MintedBlock}; - - type BlockWrapper<'b> = (u16, MintedBlock<'b>); + use super::{Header, MintedBlock, MintedBlockWithRawAuxiliary}; + + const TEST_BLOCKS: [&'static str; 25] = [ + include_str!("../../../test_data/alonzo1.block"), + include_str!("../../../test_data/alonzo2.block"), + include_str!("../../../test_data/alonzo3.block"), + include_str!("../../../test_data/alonzo4.block"), + include_str!("../../../test_data/alonzo5.block"), + include_str!("../../../test_data/alonzo6.block"), + include_str!("../../../test_data/alonzo7.block"), + include_str!("../../../test_data/alonzo8.block"), + include_str!("../../../test_data/alonzo9.block"), + // old block without invalid_transactions fields + include_str!("../../../test_data/alonzo10.block"), + // peculiar block with protocol update params + include_str!("../../../test_data/alonzo11.block"), + // peculiar block with decoding issue + // https://github.com/txpipe/oura/issues/37 + include_str!("../../../test_data/alonzo12.block"), + // peculiar block with protocol update params, including nonce + include_str!("../../../test_data/alonzo13.block"), + // peculiar block with overflow crash + // https://github.com/txpipe/oura/issues/113 + include_str!("../../../test_data/alonzo14.block"), + // peculiar block with many move-instantaneous-rewards certs + include_str!("../../../test_data/alonzo15.block"), + // peculiar block with protocol update values + include_str!("../../../test_data/alonzo16.block"), + // peculiar block with missing nonce hash + include_str!("../../../test_data/alonzo17.block"), + // peculiar block with strange AuxiliaryData variant + include_str!("../../../test_data/alonzo18.block"), + // peculiar block with strange AuxiliaryData variant + include_str!("../../../test_data/alonzo18.block"), + // peculiar block with nevative i64 overflow + include_str!("../../../test_data/alonzo19.block"), + // peculiar block with very BigInt in plutus code + include_str!("../../../test_data/alonzo20.block"), + // peculiar block with bad tx hash + include_str!("../../../test_data/alonzo21.block"), + // peculiar block with bad tx hash + include_str!("../../../test_data/alonzo22.block"), + // peculiar block with indef byte array in plutus data + include_str!("../../../test_data/alonzo23.block"), + // peculiar block with invalid address (pointer overflow) + include_str!("../../../test_data/alonzo27.block"), + ]; #[test] fn block_isomorphic_decoding_encoding() { - let test_blocks = vec![ - include_str!("../../../test_data/alonzo1.block"), - include_str!("../../../test_data/alonzo2.block"), - include_str!("../../../test_data/alonzo3.block"), - include_str!("../../../test_data/alonzo4.block"), - include_str!("../../../test_data/alonzo5.block"), - include_str!("../../../test_data/alonzo6.block"), - include_str!("../../../test_data/alonzo7.block"), - include_str!("../../../test_data/alonzo8.block"), - include_str!("../../../test_data/alonzo9.block"), - // old block without invalid_transactions fields - include_str!("../../../test_data/alonzo10.block"), - // peculiar block with protocol update params - include_str!("../../../test_data/alonzo11.block"), - // peculiar block with decoding issue - // https://github.com/txpipe/oura/issues/37 - include_str!("../../../test_data/alonzo12.block"), - // peculiar block with protocol update params, including nonce - include_str!("../../../test_data/alonzo13.block"), - // peculiar block with overflow crash - // https://github.com/txpipe/oura/issues/113 - include_str!("../../../test_data/alonzo14.block"), - // peculiar block with many move-instantaneous-rewards certs - include_str!("../../../test_data/alonzo15.block"), - // peculiar block with protocol update values - include_str!("../../../test_data/alonzo16.block"), - // peculiar block with missing nonce hash - include_str!("../../../test_data/alonzo17.block"), - // peculiar block with strange AuxiliaryData variant - include_str!("../../../test_data/alonzo18.block"), - // peculiar block with strange AuxiliaryData variant - include_str!("../../../test_data/alonzo18.block"), - // peculiar block with nevative i64 overflow - include_str!("../../../test_data/alonzo19.block"), - // peculiar block with very BigInt in plutus code - include_str!("../../../test_data/alonzo20.block"), - // peculiar block with bad tx hash - include_str!("../../../test_data/alonzo21.block"), - // peculiar block with bad tx hash - include_str!("../../../test_data/alonzo22.block"), - // peculiar block with indef byte array in plutus data - include_str!("../../../test_data/alonzo23.block"), - // peculiar block with invalid address (pointer overflow) - include_str!("../../../test_data/alonzo27.block"), - ]; + type BlockWrapper<'b> = (u16, MintedBlock<'b>); + for (idx, block_str) in TEST_BLOCKS.iter().enumerate() { + println!("decoding test block {}", idx + 1); + let bytes = hex::decode(block_str).unwrap_or_else(|_| panic!("bad block file {idx}")); - for (idx, block_str) in test_blocks.iter().enumerate() { + let block: BlockWrapper = minicbor::decode(&bytes[..]) + .unwrap_or_else(|_| panic!("error decoding cbor for file {idx}")); + + let bytes2 = to_vec(block) + .unwrap_or_else(|_| panic!("error encoding block cbor for file {idx}")); + + assert!(bytes.eq(&bytes2), "re-encoded bytes didn't match original"); + } + } + + #[test] + fn block_with_raw_aux_isomorphic_decoding_encoding() { + type BlockWrapper<'b> = (u16, MintedBlockWithRawAuxiliary<'b>); + for (idx, block_str) in TEST_BLOCKS.iter().enumerate() { println!("decoding test block {}", idx + 1); let bytes = hex::decode(block_str).unwrap_or_else(|_| panic!("bad block file {idx}")); diff --git a/pallas-primitives/src/babbage/model.rs b/pallas-primitives/src/babbage/model.rs index acc937f3..c05c81e7 100644 --- a/pallas-primitives/src/babbage/model.rs +++ b/pallas-primitives/src/babbage/model.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use pallas_codec::{ minicbor::{self, Decode, Encode}, - utils::{Bytes, CborWrap, KeepRaw, KeyValuePairs, MaybeIndefArray, Nullable}, + utils::{Bytes, CborWrap, KeepRaw, KeyValuePairs, MaybeIndefArray, Nullable, OnlyRaw}, }; use pallas_crypto::hash::{Hash, Hasher}; @@ -659,6 +659,13 @@ pub type MintedBlock<'b> = PseudoBlock< KeepRaw<'b, AuxiliaryData>, >; +pub type MintedBlockWithRawAuxiliary<'b> = PseudoBlock< + KeepRaw<'b, MintedHeader<'b>>, + KeepRaw<'b, MintedTransactionBody<'b>>, + KeepRaw<'b, MintedWitnessSet<'b>>, + OnlyRaw<'b, AuxiliaryData>, +>; + impl<'b> From> for Block { fn from(x: MintedBlock<'b>) -> Self { Block { @@ -719,6 +726,12 @@ pub type MintedTx<'b> = PseudoTx< KeepRaw<'b, AuxiliaryData>, >; +pub type MintedTxWithRawAuxiliary<'b> = PseudoTx< + KeepRaw<'b, MintedTransactionBody<'b>>, + KeepRaw<'b, MintedWitnessSet<'b>>, + OnlyRaw<'b, AuxiliaryData>, +>; + impl<'b> From> for Tx { fn from(x: MintedTx<'b>) -> Self { Tx { @@ -734,34 +747,50 @@ impl<'b> From> for Tx { mod tests { use pallas_codec::minicbor; - use super::{MintedBlock, TransactionOutput}; + use super::{MintedBlock, MintedBlockWithRawAuxiliary, TransactionOutput}; use crate::Fragment; - type BlockWrapper<'b> = (u16, MintedBlock<'b>); + const TEST_BLOCKS: [&'static str; 10] = [ + include_str!("../../../test_data/babbage1.block"), + include_str!("../../../test_data/babbage2.block"), + include_str!("../../../test_data/babbage3.block"), + // peculiar block with single plutus cost model + include_str!("../../../test_data/babbage4.block"), + // peculiar block with i32 overlfow + include_str!("../../../test_data/babbage5.block"), + // peculiar block with map undef in plutus data + include_str!("../../../test_data/babbage6.block"), + // block with generic int in cbor + include_str!("../../../test_data/babbage7.block"), + // block with indef bytes for plutus data bignum + include_str!("../../../test_data/babbage8.block"), + // block with inline datum that fails hashes + include_str!("../../../test_data/babbage9.block"), + // block with pool margin numerator greater than i64::MAX + include_str!("../../../test_data/babbage10.block"), + ]; #[test] fn block_isomorphic_decoding_encoding() { - let test_blocks = [ - include_str!("../../../test_data/babbage1.block"), - include_str!("../../../test_data/babbage2.block"), - include_str!("../../../test_data/babbage3.block"), - // peculiar block with single plutus cost model - include_str!("../../../test_data/babbage4.block"), - // peculiar block with i32 overlfow - include_str!("../../../test_data/babbage5.block"), - // peculiar block with map undef in plutus data - include_str!("../../../test_data/babbage6.block"), - // block with generic int in cbor - include_str!("../../../test_data/babbage7.block"), - // block with indef bytes for plutus data bignum - include_str!("../../../test_data/babbage8.block"), - // block with inline datum that fails hashes - include_str!("../../../test_data/babbage9.block"), - // block with pool margin numerator greater than i64::MAX - include_str!("../../../test_data/babbage10.block"), - ]; - - for (idx, block_str) in test_blocks.iter().enumerate() { + type BlockWrapper<'b> = (u16, MintedBlock<'b>); + for (idx, block_str) in TEST_BLOCKS.iter().enumerate() { + println!("decoding test block {}", idx + 1); + let bytes = hex::decode(block_str).unwrap_or_else(|_| panic!("bad block file {idx}")); + + let block: BlockWrapper = minicbor::decode(&bytes[..]) + .unwrap_or_else(|e| panic!("error decoding cbor for file {idx}: {e:?}")); + + let bytes2 = minicbor::to_vec(block) + .unwrap_or_else(|e| panic!("error encoding block cbor for file {idx}: {e:?}")); + + assert!(bytes.eq(&bytes2), "re-encoded bytes didn't match original"); + } + } + + #[test] + fn block_with_raw_aux_isomorphic_decoding_encoding() { + type BlockWrapper<'b> = (u16, MintedBlockWithRawAuxiliary<'b>); + for (idx, block_str) in TEST_BLOCKS.iter().enumerate() { println!("decoding test block {}", idx + 1); let bytes = hex::decode(block_str).unwrap_or_else(|_| panic!("bad block file {idx}")); diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index c758bade..082ddc75 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -11,7 +11,7 @@ pub use crate::{ plutus_data::*, AddrKeyhash, AssetName, Bytes, Coin, CostModel, DnsName, Epoch, ExUnits, GenesisDelegateHash, Genesishash, Hash, IPv4, IPv6, KeepRaw, KeyValuePairs, MaybeIndefArray, Metadata, Metadatum, MetadatumLabel, NetworkId, NonEmptyKeyValuePairs, NonEmptySet, NonZeroInt, - Nonce, NonceVariant, Nullable, PlutusScript, PolicyId, PoolKeyhash, PoolMetadata, + Nonce, NonceVariant, Nullable, OnlyRaw, PlutusScript, PolicyId, PoolKeyhash, PoolMetadata, PoolMetadataHash, Port, PositiveCoin, PositiveInterval, ProtocolVersion, RationalNumber, Relay, RewardAccount, ScriptHash, Set, StakeCredential, TransactionIndex, TransactionInput, UnitInterval, VrfCert, VrfKeyhash, @@ -1547,6 +1547,13 @@ pub type MintedBlock<'b> = PseudoBlock< KeepRaw<'b, AuxiliaryData>, >; +pub type MintedBlockWithRawAuxiliary<'b> = PseudoBlock< + KeepRaw<'b, MintedHeader<'b>>, + KeepRaw<'b, MintedTransactionBody<'b>>, + KeepRaw<'b, MintedWitnessSet<'b>>, + OnlyRaw<'b, AuxiliaryData>, +>; + impl<'b> From> for Block { fn from(x: MintedBlock<'b>) -> Self { Block { @@ -1607,6 +1614,12 @@ pub type MintedTx<'b> = PseudoTx< KeepRaw<'b, AuxiliaryData>, >; +pub type MintedTxWithRawAuxiliary<'b> = PseudoTx< + KeepRaw<'b, MintedTransactionBody<'b>>, + KeepRaw<'b, MintedWitnessSet<'b>>, + OnlyRaw<'b, AuxiliaryData>, +>; + impl<'b> From> for Tx { fn from(x: MintedTx<'b>) -> Self { Tx { @@ -1622,22 +1635,38 @@ impl<'b> From> for Tx { mod tests { use pallas_codec::minicbor; - use super::MintedBlock; + use super::{MintedBlock, MintedBlockWithRawAuxiliary}; - type BlockWrapper<'b> = (u16, MintedBlock<'b>); + const TEST_BLOCKS: [&'static str; 4] = [ + include_str!("../../../test_data/conway1.block"), + include_str!("../../../test_data/conway2.block"), + // interesting block with extreme values + include_str!("../../../test_data/conway3.block"), + // interesting block with extreme values + include_str!("../../../test_data/conway4.block"), + ]; #[test] fn block_isomorphic_decoding_encoding() { - let test_blocks = [ - include_str!("../../../test_data/conway1.block"), - include_str!("../../../test_data/conway2.block"), - // interesting block with extreme values - include_str!("../../../test_data/conway3.block"), - // interesting block with extreme values - include_str!("../../../test_data/conway4.block"), - ]; - - for (idx, block_str) in test_blocks.iter().enumerate() { + type BlockWrapper<'b> = (u16, MintedBlock<'b>); + for (idx, block_str) in TEST_BLOCKS.iter().enumerate() { + println!("decoding test block {}", idx + 1); + let bytes = hex::decode(block_str).unwrap_or_else(|_| panic!("bad block file {idx}")); + + let block: BlockWrapper = minicbor::decode(&bytes) + .unwrap_or_else(|e| panic!("error decoding cbor for file {idx}: {e:?}")); + + let bytes2 = minicbor::to_vec(block) + .unwrap_or_else(|e| panic!("error encoding block cbor for file {idx}: {e:?}")); + + assert!(bytes.eq(&bytes2), "re-encoded bytes didn't match original"); + } + } + + #[test] + fn block_with_raw_aux_isomorphic_decoding_encoding() { + type BlockWrapper<'b> = (u16, MintedBlockWithRawAuxiliary<'b>); + for (idx, block_str) in TEST_BLOCKS.iter().enumerate() { println!("decoding test block {}", idx + 1); let bytes = hex::decode(block_str).unwrap_or_else(|_| panic!("bad block file {idx}")); diff --git a/pallas-primitives/src/lib.rs b/pallas-primitives/src/lib.rs index accbf414..18f80066 100644 --- a/pallas-primitives/src/lib.rs +++ b/pallas-primitives/src/lib.rs @@ -13,7 +13,7 @@ pub use framework::*; pub use pallas_codec::utils::{ Bytes, Int, KeepRaw, KeyValuePairs, MaybeIndefArray, NonEmptyKeyValuePairs, NonEmptySet, - NonZeroInt, Nullable, PositiveCoin, Set, + NonZeroInt, Nullable, OnlyRaw, PositiveCoin, Set, }; pub use pallas_crypto::hash::Hash; diff --git a/pallas-traverse/src/block.rs b/pallas-traverse/src/block.rs index 5c0ff71c..c71823cf 100644 --- a/pallas-traverse/src/block.rs +++ b/pallas-traverse/src/block.rs @@ -5,7 +5,8 @@ use pallas_crypto::hash::Hash; use pallas_primitives::{alonzo, babbage, byron, conway}; use crate::{ - probe, support, Era, Error, MultiEraBlock, MultiEraHeader, MultiEraTx, MultiEraUpdate, + probe, support, Era, Error, MultiEraBlock, MultiEraBlockWithRawAuxiliary, MultiEraHeader, + MultiEraTx, MultiEraTxWithRawAuxiliary, MultiEraUpdate, }; type BlockWrapper = (u16, T); @@ -238,24 +239,259 @@ impl<'b> MultiEraBlock<'b> { } } +impl<'b> MultiEraBlockWithRawAuxiliary<'b> { + pub fn decode_epoch_boundary(cbor: &'b [u8]) -> Result { + let (_, block): BlockWrapper = + minicbor::decode(cbor).map_err(Error::invalid_cbor)?; + + Ok(Self::EpochBoundary(Box::new(block))) + } + + pub fn decode_byron(cbor: &'b [u8]) -> Result { + let (_, block): BlockWrapper = + minicbor::decode(cbor).map_err(Error::invalid_cbor)?; + + Ok(Self::Byron(Box::new(block))) + } + + pub fn decode_shelley(cbor: &'b [u8]) -> Result { + let (_, block): BlockWrapper = + minicbor::decode(cbor).map_err(Error::invalid_cbor)?; + + Ok(Self::AlonzoCompatible(Box::new(block), Era::Shelley)) + } + + pub fn decode_allegra(cbor: &'b [u8]) -> Result { + let (_, block): BlockWrapper = + minicbor::decode(cbor).map_err(Error::invalid_cbor)?; + + Ok(Self::AlonzoCompatible(Box::new(block), Era::Allegra)) + } + + pub fn decode_mary(cbor: &'b [u8]) -> Result { + let (_, block): BlockWrapper = + minicbor::decode(cbor).map_err(Error::invalid_cbor)?; + + Ok(Self::AlonzoCompatible(Box::new(block), Era::Mary)) + } + + pub fn decode_alonzo(cbor: &'b [u8]) -> Result { + let (_, block): BlockWrapper = + minicbor::decode(cbor).map_err(Error::invalid_cbor)?; + + Ok(Self::AlonzoCompatible(Box::new(block), Era::Alonzo)) + } + + pub fn decode_babbage(cbor: &'b [u8]) -> Result { + let (_, block): BlockWrapper = + minicbor::decode(cbor).map_err(Error::invalid_cbor)?; + + Ok(Self::Babbage(Box::new(block))) + } + + pub fn decode_conway(cbor: &'b [u8]) -> Result { + let (_, block): BlockWrapper = + minicbor::decode(cbor).map_err(Error::invalid_cbor)?; + + Ok(Self::Conway(Box::new(block))) + } + + pub fn decode(cbor: &'b [u8]) -> Result, Error> { + match probe::block_era(cbor) { + probe::Outcome::EpochBoundary => Self::decode_epoch_boundary(cbor), + probe::Outcome::Matched(era) => match era { + Era::Byron => Self::decode_byron(cbor), + Era::Shelley => Self::decode_shelley(cbor), + Era::Allegra => Self::decode_allegra(cbor), + Era::Mary => Self::decode_mary(cbor), + Era::Alonzo => Self::decode_alonzo(cbor), + Era::Babbage => Self::decode_babbage(cbor), + Era::Conway => Self::decode_conway(cbor), + }, + probe::Outcome::Inconclusive => Err(Error::unknown_cbor(cbor)), + } + } + + pub fn header(&self) -> MultiEraHeader<'_> { + match self { + Self::EpochBoundary(x) => MultiEraHeader::EpochBoundary(Cow::Borrowed(&x.header)), + Self::Byron(x) => MultiEraHeader::Byron(Cow::Borrowed(&x.header)), + Self::AlonzoCompatible(x, _) => { + MultiEraHeader::ShelleyCompatible(Cow::Borrowed(&x.header)) + } + Self::Babbage(x) => MultiEraHeader::BabbageCompatible(Cow::Borrowed(&x.header)), + Self::Conway(x) => MultiEraHeader::BabbageCompatible(Cow::Borrowed(&x.header)), + } + } + + /// Returns the block number (aka: height) + pub fn number(&self) -> u64 { + self.header().number() + } + + pub fn era(&self) -> Era { + match self { + Self::EpochBoundary(_) => Era::Byron, + Self::AlonzoCompatible(_, x) => *x, + Self::Babbage(_) => Era::Babbage, + Self::Byron(_) => Era::Byron, + Self::Conway(_) => Era::Conway, + } + } + + pub fn hash(&self) -> Hash<32> { + self.header().hash() + } + + pub fn slot(&self) -> u64 { + self.header().slot() + } + + /// Builds a vec with the Txs of the block + pub fn txs(&self) -> Vec { + match self { + Self::AlonzoCompatible(x, era) => support::clone_alonzo_txs_with_raw_aux(x) + .into_iter() + .map(|x| { + MultiEraTxWithRawAuxiliary::AlonzoCompatible(Box::new(Cow::Owned(x)), *era) + }) + .collect(), + Self::Babbage(x) => support::clone_babbage_txs_with_raw_aux(x) + .into_iter() + .map(|x| MultiEraTxWithRawAuxiliary::Babbage(Box::new(Cow::Owned(x)))) + .collect(), + Self::Byron(x) => support::clone_byron_txs(x) + .into_iter() + .map(|x| MultiEraTxWithRawAuxiliary::Byron(Box::new(Cow::Owned(x)))) + .collect(), + Self::Conway(x) => support::clone_conway_txs_with_raw_aux(x) + .into_iter() + .map(|x| MultiEraTxWithRawAuxiliary::Conway(Box::new(Cow::Owned(x)))) + .collect(), + Self::EpochBoundary(_) => vec![], + } + } + + /// Returns true if the there're no tx in the block + pub fn is_empty(&self) -> bool { + match self { + Self::EpochBoundary(_) => true, + Self::AlonzoCompatible(x, _) => x.transaction_bodies.is_empty(), + Self::Babbage(x) => x.transaction_bodies.is_empty(), + Self::Byron(x) => x.body.tx_payload.is_empty(), + Self::Conway(x) => x.transaction_bodies.is_empty(), + } + } + + /// Returns the count of txs in the block + pub fn tx_count(&self) -> usize { + match self { + Self::EpochBoundary(_) => 0, + Self::AlonzoCompatible(x, _) => x.transaction_bodies.len(), + Self::Babbage(x) => x.transaction_bodies.len(), + Self::Byron(x) => x.body.tx_payload.len(), + Self::Conway(x) => x.transaction_bodies.len(), + } + } + + /// Returns true if the block has any auxiliary data + pub fn has_aux_data(&self) -> bool { + match self { + Self::EpochBoundary(_) => false, + Self::AlonzoCompatible(x, _) => !x.auxiliary_data_set.is_empty(), + Self::Babbage(x) => !x.auxiliary_data_set.is_empty(), + Self::Byron(_) => false, + Self::Conway(x) => !x.auxiliary_data_set.is_empty(), + } + } + + /// Returns any block-level param update proposals (byron-specific) + pub fn update(&self) -> Option { + match self { + Self::Byron(x) => { + if let Some(up) = x.body.upd_payload.proposal.deref() { + // TODO: this might be horribly wrong, I'm assuming that the activation epoch + // for a Byron upgrade proposal is always current epoch + 1. + let epoch = x.header.consensus_data.0.epoch + 1; + Some(MultiEraUpdate::Byron( + epoch, + Box::new(Cow::Owned(up.clone())), + )) + } else { + None + } + } + _ => None, + } + } + + pub fn as_alonzo(&self) -> Option<&alonzo::MintedBlockWithRawAuxiliary> { + match self { + Self::AlonzoCompatible(x, _) => Some(x), + _ => None, + } + } + + pub fn as_babbage(&self) -> Option<&babbage::MintedBlockWithRawAuxiliary> { + match self { + Self::Babbage(x) => Some(x), + _ => None, + } + } + + pub fn as_byron(&self) -> Option<&byron::MintedBlock> { + match self { + Self::Byron(x) => Some(x), + _ => None, + } + } + + pub fn as_conway(&self) -> Option<&conway::MintedBlockWithRawAuxiliary> { + match self { + Self::Conway(x) => Some(x), + _ => None, + } + } + + /// Return the size of the serialised block in bytes + pub fn size(&self) -> usize { + match self { + Self::EpochBoundary(b) => minicbor::to_vec(b).unwrap().len(), + Self::Byron(b) => minicbor::to_vec(b).unwrap().len(), + Self::AlonzoCompatible(b, _) => minicbor::to_vec(b).unwrap().len(), + Self::Babbage(b) => minicbor::to_vec(b).unwrap().len(), + Self::Conway(b) => minicbor::to_vec(b).unwrap().len(), + } + } +} + #[cfg(test)] mod tests { use super::*; + const TEST_BLOCKS: [(&'static str, usize); 5] = [ + (include_str!("../../test_data/byron2.block"), 2usize), + (include_str!("../../test_data/shelley1.block"), 4), + (include_str!("../../test_data/mary1.block"), 14), + (include_str!("../../test_data/allegra1.block"), 3), + (include_str!("../../test_data/alonzo1.block"), 5), + ]; + #[test] fn test_iteration() { - let blocks = vec![ - (include_str!("../../test_data/byron2.block"), 2usize), - (include_str!("../../test_data/shelley1.block"), 4), - (include_str!("../../test_data/mary1.block"), 14), - (include_str!("../../test_data/allegra1.block"), 3), - (include_str!("../../test_data/alonzo1.block"), 5), - ]; - - for (block_str, tx_count) in blocks.into_iter() { + for (block_str, tx_count) in TEST_BLOCKS.into_iter() { let cbor = hex::decode(block_str).expect("invalid hex"); let block = MultiEraBlock::decode(&cbor).expect("invalid cbor"); assert_eq!(block.txs().len(), tx_count); } } + + #[test] + fn test_with_raw_aux_iteration() { + for (block_str, tx_count) in TEST_BLOCKS.into_iter() { + let cbor = hex::decode(block_str).expect("invalid hex"); + let block = MultiEraBlockWithRawAuxiliary::decode(&cbor).expect("invalid cbor"); + assert_eq!(block.txs().len(), tx_count); + } + } } diff --git a/pallas-traverse/src/lib.rs b/pallas-traverse/src/lib.rs index bce18563..7b05e20b 100644 --- a/pallas-traverse/src/lib.rs +++ b/pallas-traverse/src/lib.rs @@ -81,6 +81,16 @@ pub enum MultiEraBlock<'b> { Conway(Box>), } +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum MultiEraBlockWithRawAuxiliary<'b> { + EpochBoundary(Box>), + AlonzoCompatible(Box>, Era), + Babbage(Box>), + Byron(Box>), + Conway(Box>), +} + #[derive(Debug, Clone)] #[non_exhaustive] pub enum MultiEraTx<'b> { @@ -90,6 +100,15 @@ pub enum MultiEraTx<'b> { Conway(Box>>), } +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum MultiEraTxWithRawAuxiliary<'b> { + AlonzoCompatible(Box>>, Era), + Babbage(Box>>), + Byron(Box>>), + Conway(Box>>), +} + #[derive(Debug, Clone)] #[non_exhaustive] pub enum MultiEraValue<'b> { diff --git a/pallas-traverse/src/size.rs b/pallas-traverse/src/size.rs index b978d1e5..efbebbdf 100644 --- a/pallas-traverse/src/size.rs +++ b/pallas-traverse/src/size.rs @@ -1,6 +1,6 @@ use pallas_codec::utils::Nullable; -use crate::{MultiEraBlock, MultiEraTx}; +use crate::{MultiEraBlock, MultiEraBlockWithRawAuxiliary, MultiEraTx, MultiEraTxWithRawAuxiliary}; impl<'b> MultiEraTx<'b> { fn aux_data_size(&self) -> usize { @@ -44,6 +44,48 @@ impl<'b> MultiEraTx<'b> { } } +impl<'b> MultiEraTxWithRawAuxiliary<'b> { + fn aux_data_size(&self) -> usize { + match self { + Self::AlonzoCompatible(x, _) => match &x.auxiliary_data { + Nullable::Some(x) => x.raw_cbor().len(), + _ => 2, + }, + Self::Babbage(x) => match &x.auxiliary_data { + Nullable::Some(x) => x.raw_cbor().len(), + _ => 2, + }, + Self::Byron(_) => 0, + Self::Conway(x) => match &x.auxiliary_data { + Nullable::Some(x) => x.raw_cbor().len(), + _ => 2, + }, + } + } + + fn body_size(&self) -> usize { + match self { + Self::AlonzoCompatible(x, _) => x.transaction_body.raw_cbor().len(), + Self::Babbage(x) => x.transaction_body.raw_cbor().len(), + Self::Byron(x) => x.transaction.raw_cbor().len(), + Self::Conway(x) => x.transaction_body.raw_cbor().len(), + } + } + + fn witness_set_size(&self) -> usize { + match self { + Self::AlonzoCompatible(x, _) => x.transaction_witness_set.raw_cbor().len(), + Self::Babbage(x) => x.transaction_witness_set.raw_cbor().len(), + Self::Byron(x) => x.witness.raw_cbor().len(), + Self::Conway(x) => x.transaction_witness_set.raw_cbor().len(), + } + } + + pub fn size(&self) -> usize { + self.body_size() + self.witness_set_size() + self.aux_data_size() + } +} + impl<'b> MultiEraBlock<'b> { pub fn body_size(&self) -> Option { match self { @@ -57,3 +99,15 @@ impl<'b> MultiEraBlock<'b> { } } } + +impl<'b> MultiEraBlockWithRawAuxiliary<'b> { + pub fn body_size(&self) -> Option { + match self { + Self::AlonzoCompatible(x, _) => Some(x.header.header_body.block_body_size as usize), + Self::Babbage(x) => Some(x.header.header_body.block_body_size as usize), + Self::EpochBoundary(_) => None, + Self::Byron(_) => None, + Self::Conway(x) => Some(x.header.header_body.block_body_size as usize), + } + } +} diff --git a/pallas-traverse/src/support.rs b/pallas-traverse/src/support.rs index 8465a3ec..4a8e8f00 100644 --- a/pallas-traverse/src/support.rs +++ b/pallas-traverse/src/support.rs @@ -3,8 +3,8 @@ use pallas_primitives::{alonzo, babbage, byron, conway}; macro_rules! clone_tx_fn { - ($fn_name:ident, $era:tt) => { - fn $fn_name<'b>(block: &'b $era::MintedBlock, index: usize) -> Option<$era::MintedTx<'b>> { + ($fn_name:ident, $era:tt, $block_type:ident, $tx_type:ident) => { + fn $fn_name<'b>(block: &'b $era::$block_type, index: usize) -> Option<$era::$tx_type<'b>> { let transaction_body = block.transaction_bodies.get(index).cloned()?; let transaction_witness_set = block.transaction_witness_sets.get(index)?.clone(); @@ -28,7 +28,7 @@ macro_rules! clone_tx_fn { .cloned() .into(); - let x = $era::MintedTx { + let x = $era::$tx_type { transaction_body, transaction_witness_set, success, @@ -40,9 +40,27 @@ macro_rules! clone_tx_fn { }; } -clone_tx_fn!(conway_clone_tx_at, conway); -clone_tx_fn!(babbage_clone_tx_at, babbage); -clone_tx_fn!(alonzo_clone_tx_at, alonzo); +clone_tx_fn!(conway_clone_tx_at, conway, MintedBlock, MintedTx); +clone_tx_fn!( + conway_clone_tx_with_raw_aux_at, + conway, + MintedBlockWithRawAuxiliary, + MintedTxWithRawAuxiliary +); +clone_tx_fn!(babbage_clone_tx_at, babbage, MintedBlock, MintedTx); +clone_tx_fn!( + babbage_clone_tx_with_raw_aux_at, + babbage, + MintedBlockWithRawAuxiliary, + MintedTxWithRawAuxiliary +); +clone_tx_fn!(alonzo_clone_tx_at, alonzo, MintedBlock, MintedTx); +clone_tx_fn!( + alonzo_clone_tx_with_raw_aux_at, + alonzo, + MintedBlockWithRawAuxiliary, + MintedTxWithRawAuxiliary +); pub fn clone_alonzo_txs<'b>(block: &'b alonzo::MintedBlock) -> Vec> { (0..block.transaction_bodies.len()) @@ -51,6 +69,15 @@ pub fn clone_alonzo_txs<'b>(block: &'b alonzo::MintedBlock) -> Vec( + block: &'b alonzo::MintedBlockWithRawAuxiliary, +) -> Vec> { + (0..block.transaction_bodies.len()) + .step_by(1) + .filter_map(|idx| alonzo_clone_tx_with_raw_aux_at(block, idx)) + .collect() +} + pub fn clone_babbage_txs<'b>(block: &'b babbage::MintedBlock) -> Vec> { (0..block.transaction_bodies.len()) .step_by(1) @@ -58,6 +85,15 @@ pub fn clone_babbage_txs<'b>(block: &'b babbage::MintedBlock) -> Vec( + block: &'b babbage::MintedBlockWithRawAuxiliary, +) -> Vec> { + (0..block.transaction_bodies.len()) + .step_by(1) + .filter_map(|idx| babbage_clone_tx_with_raw_aux_at(block, idx)) + .collect() +} + pub fn clone_conway_txs<'b>(block: &'b conway::MintedBlock) -> Vec> { (0..block.transaction_bodies.len()) .step_by(1) @@ -65,6 +101,15 @@ pub fn clone_conway_txs<'b>(block: &'b conway::MintedBlock) -> Vec( + block: &'b conway::MintedBlockWithRawAuxiliary, +) -> Vec> { + (0..block.transaction_bodies.len()) + .step_by(1) + .filter_map(|idx| conway_clone_tx_with_raw_aux_at(block, idx)) + .collect() +} + pub fn clone_byron_txs<'b>(block: &'b byron::MintedBlock) -> Vec> { block.body.tx_payload.iter().cloned().collect() } diff --git a/pallas-traverse/src/time.rs b/pallas-traverse/src/time.rs index a5b8c0c7..62b7845d 100644 --- a/pallas-traverse/src/time.rs +++ b/pallas-traverse/src/time.rs @@ -1,4 +1,4 @@ -use crate::{wellknown::GenesisValues, MultiEraBlock}; +use crate::{wellknown::GenesisValues, MultiEraBlock, MultiEraBlockWithRawAuxiliary}; pub type Epoch = u64; @@ -144,6 +144,29 @@ impl<'a> MultiEraBlock<'a> { } } +impl<'a> MultiEraBlockWithRawAuxiliary<'a> { + pub fn epoch(&self, genesis: &GenesisValues) -> (Epoch, SubSlot) { + match self { + Self::EpochBoundary(x) => (x.header.consensus_data.epoch_id, 0), + Self::Byron(x) => ( + x.header.consensus_data.0.epoch, + x.header.consensus_data.0.slot, + ), + Self::AlonzoCompatible(x, _) => { + genesis.absolute_slot_to_relative(x.header.header_body.slot) + } + Self::Babbage(x) => genesis.absolute_slot_to_relative(x.header.header_body.slot), + Self::Conway(x) => genesis.absolute_slot_to_relative(x.header.header_body.slot), + } + } + + /// Computes the unix timestamp for the slot of the tx + pub fn wallclock(&self, genesis: &GenesisValues) -> u64 { + let slot = self.slot(); + genesis.slot_to_wallclock(slot) + } +} + #[cfg(test)] mod tests { use super::*; @@ -270,5 +293,18 @@ mod tests { ); assert_eq!(computed_slot, 4492794); + + let block = MultiEraBlockWithRawAuxiliary::decode(&block_cbor).expect("invalid cbor"); + + let byron = block.as_byron().unwrap(); + + let genesis = GenesisValues::default(); + + let computed_slot = genesis.relative_slot_to_absolute( + byron.header.consensus_data.0.epoch, + byron.header.consensus_data.0.slot, + ); + + assert_eq!(computed_slot, 4492794); } } diff --git a/pallas-traverse/src/tx.rs b/pallas-traverse/src/tx.rs index 21b1db40..ee420555 100644 --- a/pallas-traverse/src/tx.rs +++ b/pallas-traverse/src/tx.rs @@ -6,12 +6,13 @@ use pallas_crypto::hash::Hash; use pallas_primitives::{ alonzo, babbage::{self, NetworkId}, - byron, conway, + byron, conway, OnlyRaw, }; use crate::{ Era, Error, MultiEraCert, MultiEraInput, MultiEraMeta, MultiEraOutput, MultiEraPolicyAssets, - MultiEraSigners, MultiEraTx, MultiEraUpdate, MultiEraWithdrawals, OriginalHash, + MultiEraSigners, MultiEraTx, MultiEraTxWithRawAuxiliary, MultiEraUpdate, MultiEraWithdrawals, + OriginalHash, }; impl<'b> MultiEraTx<'b> { @@ -625,3 +626,598 @@ impl<'b> MultiEraTx<'b> { } } } + +impl<'b> MultiEraTxWithRawAuxiliary<'b> { + pub fn from_byron(tx: &'b byron::MintedTxPayload<'b>) -> Self { + Self::Byron(Box::new(Cow::Borrowed(tx))) + } + + pub fn from_alonzo_compatible(tx: &'b alonzo::MintedTxWithRawAuxiliary<'b>, era: Era) -> Self { + Self::AlonzoCompatible(Box::new(Cow::Borrowed(tx)), era) + } + + pub fn from_babbage(tx: &'b babbage::MintedTxWithRawAuxiliary<'b>) -> Self { + Self::Babbage(Box::new(Cow::Borrowed(tx))) + } + + pub fn encode(&self) -> Vec { + // to_vec is infallible + match self { + Self::AlonzoCompatible(x, _) => minicbor::to_vec(x).unwrap(), + Self::Babbage(x) => minicbor::to_vec(x).unwrap(), + Self::Byron(x) => minicbor::to_vec(x).unwrap(), + Self::Conway(x) => minicbor::to_vec(x).unwrap(), + } + } + + pub fn decode_for_era(era: Era, cbor: &'b [u8]) -> Result { + match era { + Era::Byron => { + let tx = minicbor::decode(cbor)?; + let tx = Box::new(Cow::Owned(tx)); + Ok(Self::Byron(tx)) + } + Era::Shelley | Era::Allegra | Era::Mary | Era::Alonzo => { + let tx = minicbor::decode(cbor)?; + let tx = Box::new(Cow::Owned(tx)); + Ok(Self::AlonzoCompatible(tx, era)) + } + Era::Babbage => { + let tx = minicbor::decode(cbor)?; + let tx = Box::new(Cow::Owned(tx)); + Ok(Self::Babbage(tx)) + } + Era::Conway => { + let tx = minicbor::decode(cbor)?; + let tx = Box::new(Cow::Owned(tx)); + Ok(Self::Conway(tx)) + } + } + } + + /// Try decode a transaction via every era's encoding format, starting with + /// the most recent and returning on first success, or None if none are + /// successful + /// + /// NOTE: Until Conway is officially released, this method favors Babbage + /// decoding over Conway decoding. This means that we'll attempt to + /// decode using Babbage first even if Conway is newer. + pub fn decode(cbor: &'b [u8]) -> Result { + if let Ok(tx) = minicbor::decode(cbor) { + return Ok(Self::Conway(Box::new(Cow::Owned(tx)))); + } + + if let Ok(tx) = minicbor::decode(cbor) { + return Ok(Self::Babbage(Box::new(Cow::Owned(tx)))); + } + + if let Ok(tx) = minicbor::decode(cbor) { + // Shelley/Allegra/Mary/Alonzo will all decode to Alonzo + return Ok(Self::AlonzoCompatible( + Box::new(Cow::Owned(tx)), + Era::Alonzo, + )); + } + + if let Ok(tx) = minicbor::decode(cbor) { + Ok(Self::Byron(Box::new(Cow::Owned(tx)))) + } else { + Err(Error::unknown_cbor(cbor)) + } + } + + pub fn era(&self) -> Era { + match self { + Self::AlonzoCompatible(_, era) => *era, + Self::Babbage(_) => Era::Babbage, + Self::Byron(_) => Era::Byron, + Self::Conway(_) => Era::Conway, + } + } + + pub fn hash(&self) -> Hash<32> { + match self { + Self::AlonzoCompatible(x, _) => x.transaction_body.original_hash(), + Self::Babbage(x) => x.transaction_body.original_hash(), + Self::Byron(x) => x.transaction.original_hash(), + Self::Conway(x) => x.transaction_body.original_hash(), + } + } + + pub fn outputs(&self) -> Vec { + match self { + Self::AlonzoCompatible(x, _) => x + .transaction_body + .outputs + .iter() + .map(|x| MultiEraOutput::from_alonzo_compatible(x, self.era())) + .collect(), + Self::Babbage(x) => x + .transaction_body + .outputs + .iter() + .map(MultiEraOutput::from_babbage) + .collect(), + Self::Byron(x) => x + .transaction + .outputs + .iter() + .map(MultiEraOutput::from_byron) + .collect(), + Self::Conway(x) => x + .transaction_body + .outputs + .iter() + .map(MultiEraOutput::from_conway) + .collect(), + } + } + + pub fn output_at(&self, index: usize) -> Option { + match self { + Self::AlonzoCompatible(x, _) => x + .transaction_body + .outputs + .get(index) + .map(|x| MultiEraOutput::from_alonzo_compatible(x, self.era())), + Self::Babbage(x) => x + .transaction_body + .outputs + .get(index) + .map(MultiEraOutput::from_babbage), + Self::Byron(x) => x + .transaction + .outputs + .get(index) + .map(MultiEraOutput::from_byron), + Self::Conway(x) => x + .transaction_body + .outputs + .get(index) + .map(MultiEraOutput::from_conway), + } + } + + /// Return the transaction inputs + /// + /// NOTE: It is possible for this to return duplicates before some point in the chain history. See https://github.com/input-output-hk/cardano-ledger/commit/a342b74f5db3d3a75eae3e2abe358a169701b1e7 + pub fn inputs(&self) -> Vec { + match self { + Self::AlonzoCompatible(x, _) => x + .transaction_body + .inputs + .iter() + .map(MultiEraInput::from_alonzo_compatible) + .collect(), + Self::Babbage(x) => x + .transaction_body + .inputs + .iter() + .map(MultiEraInput::from_alonzo_compatible) + .collect(), + Self::Byron(x) => x + .transaction + .inputs + .iter() + .map(MultiEraInput::from_byron) + .collect(), + Self::Conway(x) => x + .transaction_body + .inputs + .iter() + .map(MultiEraInput::from_alonzo_compatible) + .collect(), + } + } + + /// Return inputs as expected for processing + /// + /// To process inputs we need a set (no duplicated) and lexicographical + /// order (hash#idx). This function will take the raw inputs and apply the + /// aforementioned cleanup changes. + pub fn inputs_sorted_set(&self) -> Vec { + let mut raw = self.inputs(); + raw.sort_by_key(|x| x.lexicographical_key()); + raw.dedup_by_key(|x| x.lexicographical_key()); + + raw + } + + pub fn mints_sorted_set(&self) -> Vec { + let mut raw = self.mints(); + + raw.sort_by_key(|m| *m.policy()); + + raw + } + + pub fn withdrawals_sorted_set(&self) -> Vec<(&[u8], u64)> { + match self.withdrawals() { + MultiEraWithdrawals::NotApplicable | MultiEraWithdrawals::Empty => { + std::iter::empty().collect() + } + MultiEraWithdrawals::AlonzoCompatible(x) => x + .iter() + .map(|(k, v)| (k.as_slice(), *v)) + .sorted_by_key(|(k, _)| *k) + .collect(), + MultiEraWithdrawals::Conway(x) => x + .iter() + .map(|(k, v)| (k.as_slice(), *v)) + .sorted_by_key(|(k, _)| *k) + .collect(), + } + } + + /// Return the transaction reference inputs + /// + /// NOTE: It is possible for this to return duplicates. See + /// https://github.com/input-output-hk/cardano-ledger/commit/a342b74f5db3d3a75eae3e2abe358a169701b1e7 + pub fn reference_inputs(&self) -> Vec { + match self { + Self::Conway(x) => x + .transaction_body + .reference_inputs + .iter() + .flatten() + .map(MultiEraInput::from_alonzo_compatible) + .collect(), + Self::Babbage(x) => x + .transaction_body + .reference_inputs + .iter() + .flatten() + .map(MultiEraInput::from_alonzo_compatible) + .collect(), + _ => vec![], + } + } + + pub fn certs(&self) -> Vec { + match self { + Self::AlonzoCompatible(x, _) => x + .transaction_body + .certificates + .iter() + .flat_map(|c| c.iter()) + .map(|c| MultiEraCert::AlonzoCompatible(Box::new(Cow::Borrowed(c)))) + .collect(), + Self::Babbage(x) => x + .transaction_body + .certificates + .iter() + .flat_map(|c| c.iter()) + .map(|c| MultiEraCert::AlonzoCompatible(Box::new(Cow::Borrowed(c)))) + .collect(), + Self::Byron(_) => vec![], + Self::Conway(x) => x + .transaction_body + .certificates + .iter() + .flat_map(|c| c.iter()) + .map(|c| MultiEraCert::Conway(Box::new(Cow::Borrowed(c)))) + .collect(), + } + } + + pub fn update(&self) -> Option { + match self { + Self::AlonzoCompatible(x, _) => x + .transaction_body + .update + .as_ref() + .map(MultiEraUpdate::from_alonzo_compatible), + Self::Babbage(x) => x + .transaction_body + .update + .as_ref() + .map(MultiEraUpdate::from_babbage), + Self::Byron(_) => None, + Self::Conway(_) => None, + } + } + + pub fn mints(&self) -> Vec { + match self { + Self::Byron(_) => vec![], + Self::AlonzoCompatible(x, _) => x + .transaction_body + .mint + .iter() + .flat_map(|x| x.iter()) + .map(|(k, v)| MultiEraPolicyAssets::AlonzoCompatibleMint(k, v)) + .collect(), + Self::Babbage(x) => x + .transaction_body + .mint + .iter() + .flat_map(|x| x.iter()) + .map(|(k, v)| MultiEraPolicyAssets::AlonzoCompatibleMint(k, v)) + .collect(), + Self::Conway(x) => x + .transaction_body + .mint + .iter() + .flat_map(|x| x.iter()) + .map(|(k, v)| MultiEraPolicyAssets::ConwayMint(k, v)) + .collect(), + } + } + + /// Return the transaction collateral inputs + /// + /// NOTE: It is possible for this to return duplicates. See + /// https://github.com/input-output-hk/cardano-ledger/commit/a342b74f5db3d3a75eae3e2abe358a169701b1e7 + pub fn collateral(&self) -> Vec { + match self { + Self::Byron(_) => vec![], + Self::AlonzoCompatible(x, _) => x + .transaction_body + .collateral + .iter() + .flat_map(|x| x.iter()) + .map(MultiEraInput::from_alonzo_compatible) + .collect(), + Self::Babbage(x) => x + .transaction_body + .collateral + .iter() + .flat_map(|x| x.iter()) + .map(MultiEraInput::from_alonzo_compatible) + .collect(), + Self::Conway(x) => x + .transaction_body + .collateral + .iter() + .flat_map(|x| x.iter()) + .map(MultiEraInput::from_alonzo_compatible) + .collect(), + } + } + + pub fn collateral_return(&self) -> Option { + match self { + Self::Babbage(x) => x + .transaction_body + .collateral_return + .as_ref() + .map(MultiEraOutput::from_babbage), + Self::Conway(x) => x + .transaction_body + .collateral_return + .as_ref() + .map(MultiEraOutput::from_conway), + _ => None, + } + } + + pub fn total_collateral(&self) -> Option { + match self { + Self::Babbage(x) => x.transaction_body.total_collateral, + Self::Conway(x) => x.transaction_body.total_collateral, + _ => None, + } + } + + /// Returns the list of inputs consumed by the Tx + /// + /// Helper method to abstract the logic of which inputs are consumed + /// depending on the validity of the Tx. If the Tx is valid, this method + /// will return the list of inputs. If the tx is invalid, it will return the + /// collateral. + pub fn consumes(&self) -> Vec { + let consumed = match self.is_valid() { + true => self.inputs(), + false => self.collateral(), + }; + + let mut unique_consumed = HashSet::new(); + + consumed + .into_iter() + .filter(|i| unique_consumed.insert(i.output_ref())) + .collect() + } + + /// Returns a list of tuples of the outputs produced by the Tx with their + /// indexes + /// + /// Helper method to abstract the logic of which outputs are produced + /// depending on the validity of the Tx. If the Tx is valid, this method + /// will return the list of outputs. If the Tx is invalid it will return the + /// collateral return if one is present or an empty list if not. Note that + /// the collateral return output index is defined as the next available + /// index after the txouts (Babbage spec, ch 4). + pub fn produces(&self) -> Vec<(usize, MultiEraOutput)> { + match self.is_valid() { + true => self.outputs().into_iter().enumerate().collect(), + false => self + .collateral_return() + .into_iter() + .map(|txo| (self.outputs().len(), txo)) + .collect(), + } + } + + /// Returns the *produced* output at the given index if one exists + /// + /// If the transaction is valid the outputs are produced, otherwise the + /// collateral return output is produced at index |outputs.len()| if one is + /// present. This function gets the *produced* output for an index if one + /// exists. It behaves exactly as `outputs_at` for valid transactions, but + /// for invalid transactions it returns None except for if the index points + /// to the collateral-return output and one is present in the transaction, + /// in which case it returns the collateral-return output. + pub fn produces_at(&self, index: usize) -> Option { + match self.is_valid() { + true => self.output_at(index), + false => { + if index == self.outputs().len() { + self.collateral_return() + } else { + None + } + } + } + } + + /// Returns the list of UTxO required by the Tx + /// + /// Helper method to yield all of the UTxO that the Tx requires in order to + /// be fulfilled. This includes normal inputs, reference inputs and + /// collateral. + pub fn requires(&self) -> Vec { + [self.inputs(), self.reference_inputs(), self.collateral()].concat() + } + + pub fn withdrawals(&self) -> MultiEraWithdrawals { + match self { + Self::AlonzoCompatible(x, _) => match &x.transaction_body.withdrawals { + Some(x) => MultiEraWithdrawals::AlonzoCompatible(x), + None => MultiEraWithdrawals::Empty, + }, + Self::Babbage(x) => match &x.transaction_body.withdrawals { + Some(x) => MultiEraWithdrawals::AlonzoCompatible(x), + None => MultiEraWithdrawals::Empty, + }, + Self::Byron(_) => MultiEraWithdrawals::NotApplicable, + Self::Conway(x) => match &x.transaction_body.withdrawals { + Some(x) => MultiEraWithdrawals::Conway(x), + None => MultiEraWithdrawals::Empty, + }, + } + } + + pub fn fee(&self) -> Option { + match self { + Self::AlonzoCompatible(x, _) => Some(x.transaction_body.fee), + Self::Babbage(x) => Some(x.transaction_body.fee), + Self::Byron(_) => None, + Self::Conway(x) => Some(x.transaction_body.fee), + } + } + + pub fn ttl(&self) -> Option { + match self { + Self::AlonzoCompatible(x, _) => x.transaction_body.ttl, + Self::Babbage(x) => x.transaction_body.ttl, + Self::Byron(_) => None, + Self::Conway(x) => x.transaction_body.ttl, + } + } + + /// Returns the fee or attempts to compute it + /// + /// If the fee is available as part of the tx data (post-byron), this + /// function will return the existing value. For byron txs, this method + /// attempts to compute the value by using the linear fee policy. + #[cfg(feature = "unstable")] + pub fn fee_or_compute(&self) -> u64 { + match self { + Self::AlonzoCompatible(x, _) => x.transaction_body.fee, + Self::Babbage(x) => x.transaction_body.fee, + Self::Byron(x) => crate::fees::compute_byron_fee(x, None), + Self::Conway(x) => x.transaction_body.fee, + } + } + + pub fn aux_data(&self) -> Option<&OnlyRaw<'_, alonzo::AuxiliaryData>> { + match self { + Self::AlonzoCompatible(x, _) => match &x.auxiliary_data { + pallas_codec::utils::Nullable::Some(x) => Some(x), + pallas_codec::utils::Nullable::Null => None, + pallas_codec::utils::Nullable::Undefined => None, + }, + Self::Babbage(x) => match &x.auxiliary_data { + pallas_codec::utils::Nullable::Some(x) => Some(x), + pallas_codec::utils::Nullable::Null => None, + pallas_codec::utils::Nullable::Undefined => None, + }, + Self::Byron(_) => None, + Self::Conway(x) => match &x.auxiliary_data { + pallas_codec::utils::Nullable::Some(x) => Some(x), + pallas_codec::utils::Nullable::Null => None, + pallas_codec::utils::Nullable::Undefined => None, + }, + } + } + + pub fn required_signers(&self) -> MultiEraSigners { + match self { + Self::AlonzoCompatible(x, _) => x + .transaction_body + .required_signers + .as_ref() + .map(MultiEraSigners::AlonzoCompatible) + .unwrap_or_default(), + Self::Babbage(x) => x + .transaction_body + .required_signers + .as_ref() + .map(MultiEraSigners::AlonzoCompatible) + .unwrap_or_default(), + Self::Byron(_) => MultiEraSigners::NotApplicable, + Self::Conway(x) => x + .transaction_body + .required_signers + .as_ref() + .map(|x| MultiEraSigners::AlonzoCompatible(x.deref())) + .unwrap_or_default(), + } + } + + pub fn validity_start(&self) -> Option { + match self { + Self::AlonzoCompatible(x, _) => x.transaction_body.validity_interval_start, + Self::Babbage(x) => x.transaction_body.validity_interval_start, + Self::Byron(_) => None, + Self::Conway(x) => x.transaction_body.validity_interval_start, + } + } + + pub fn network_id(&self) -> Option { + match self { + Self::AlonzoCompatible(x, _) => x.transaction_body.network_id, + Self::Babbage(x) => x.transaction_body.network_id, + Self::Byron(_) => None, + Self::Conway(x) => x.transaction_body.network_id, + } + } + + pub fn is_valid(&self) -> bool { + match self { + Self::AlonzoCompatible(x, _) => x.success, + Self::Babbage(x) => x.success, + Self::Byron(_) => true, + Self::Conway(x) => x.success, + } + } + + pub fn as_babbage(&self) -> Option<&babbage::MintedTxWithRawAuxiliary> { + match self { + Self::Babbage(x) => Some(x), + _ => None, + } + } + + pub fn as_alonzo(&self) -> Option<&alonzo::MintedTxWithRawAuxiliary> { + match self { + Self::AlonzoCompatible(x, _) => Some(x), + _ => None, + } + } + + pub fn as_byron(&self) -> Option<&byron::MintedTxPayload> { + match self { + Self::Byron(x) => Some(x), + _ => None, + } + } + + pub fn as_conway(&self) -> Option<&conway::MintedTxWithRawAuxiliary> { + match self { + Self::Conway(x) => Some(x), + _ => None, + } + } +} diff --git a/pallas-traverse/src/witnesses.rs b/pallas-traverse/src/witnesses.rs index 9c8cf3e7..ee36256a 100644 --- a/pallas-traverse/src/witnesses.rs +++ b/pallas-traverse/src/witnesses.rs @@ -4,7 +4,7 @@ use pallas_primitives::{ conway, PlutusData, PlutusScript, }; -use crate::{MultiEraRedeemer, MultiEraTx}; +use crate::{MultiEraRedeemer, MultiEraTx, MultiEraTxWithRawAuxiliary}; impl<'b> MultiEraTx<'b> { pub fn vkey_witnesses(&self) -> &[VKeyWitness] { @@ -217,3 +217,215 @@ impl<'b> MultiEraTx<'b> { } } } + +impl<'b> MultiEraTxWithRawAuxiliary<'b> { + pub fn vkey_witnesses(&self) -> &[VKeyWitness] { + match self { + Self::Byron(_) => &[], + Self::AlonzoCompatible(x, _) => x + .transaction_witness_set + .vkeywitness + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + Self::Babbage(x) => x + .transaction_witness_set + .vkeywitness + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + Self::Conway(x) => x + .transaction_witness_set + .vkeywitness + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + } + } + + pub fn native_scripts(&self) -> &[KeepRaw<'b, NativeScript>] { + match self { + Self::Byron(_) => &[], + Self::AlonzoCompatible(x, _) => x + .transaction_witness_set + .native_script + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + Self::Babbage(x) => x + .transaction_witness_set + .native_script + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + Self::Conway(x) => x + .transaction_witness_set + .native_script + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + } + } + + pub fn bootstrap_witnesses(&self) -> &[BootstrapWitness] { + match self { + Self::Byron(_) => &[], + Self::AlonzoCompatible(x, _) => x + .transaction_witness_set + .bootstrap_witness + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + Self::Babbage(x) => x + .transaction_witness_set + .bootstrap_witness + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + Self::Conway(x) => x + .transaction_witness_set + .bootstrap_witness + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + } + } + + pub fn plutus_v1_scripts(&self) -> &[alonzo::PlutusScript<1>] { + match self { + Self::Byron(_) => &[], + Self::AlonzoCompatible(x, _) => x + .transaction_witness_set + .plutus_script + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + Self::Babbage(x) => x + .transaction_witness_set + .plutus_v1_script + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + Self::Conway(x) => x + .transaction_witness_set + .plutus_v1_script + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + } + } + + pub fn plutus_data(&self) -> &[KeepRaw<'b, PlutusData>] { + match self { + Self::Byron(_) => &[], + Self::AlonzoCompatible(x, _) => x + .transaction_witness_set + .plutus_data + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + Self::Babbage(x) => x + .transaction_witness_set + .plutus_data + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + Self::Conway(x) => x + .transaction_witness_set + .plutus_data + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + } + } + + pub fn redeemers(&self) -> Vec { + match self { + Self::Byron(_) => vec![], + Self::AlonzoCompatible(x, _) => x + .transaction_witness_set + .redeemer + .iter() + .flat_map(|x| x.iter()) + .map(MultiEraRedeemer::from_alonzo_compatible) + .collect(), + Self::Babbage(x) => x + .transaction_witness_set + .redeemer + .iter() + .flat_map(|x| x.iter()) + .map(MultiEraRedeemer::from_alonzo_compatible) + .collect(), + Self::Conway(x) => match x.transaction_witness_set.redeemer.as_deref() { + Some(conway::Redeemers::Map(x)) => x + .iter() + .map(|(k, v)| MultiEraRedeemer::from_conway(k, v)) + .collect(), + Some(conway::Redeemers::List(x)) => x + .iter() + .map(MultiEraRedeemer::from_conway_deprecated) + .collect(), + _ => vec![], + }, + } + } + + pub fn find_spend_redeemer(&self, input_order: u32) -> Option { + self.redeemers().into_iter().find(|r| { + r.tag() == pallas_primitives::conway::RedeemerTag::Spend && r.index() == input_order + }) + } + + pub fn find_mint_redeemer(&self, mint_order: u32) -> Option { + self.redeemers().into_iter().find(|r| { + r.tag() == pallas_primitives::conway::RedeemerTag::Mint && r.index() == mint_order + }) + } + + pub fn find_withdrawal_redeemer(&self, withdrawal_order: u32) -> Option { + self.redeemers().into_iter().find(|r| { + r.tag() == pallas_primitives::conway::RedeemerTag::Reward + && r.index() == withdrawal_order + }) + } + + pub fn find_certificate_redeemer(&self, certificate_order: u32) -> Option { + self.redeemers().into_iter().find(|r| { + r.tag() == pallas_primitives::conway::RedeemerTag::Cert + && r.index() == certificate_order + }) + } + + pub fn plutus_v2_scripts(&self) -> &[PlutusScript<2>] { + match self { + Self::Byron(_) => &[], + Self::AlonzoCompatible(_, _) => &[], + Self::Babbage(x) => x + .transaction_witness_set + .plutus_v2_script + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + Self::Conway(x) => x + .transaction_witness_set + .plutus_v2_script + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + } + } + + pub fn plutus_v3_scripts(&self) -> &[PlutusScript<3>] { + match self { + Self::Byron(_) => &[], + Self::AlonzoCompatible(_, _) => &[], + Self::Babbage(_) => &[], + Self::Conway(x) => x + .transaction_witness_set + .plutus_v3_script + .as_ref() + .map(|x| x.as_ref()) + .unwrap_or(&[]), + } + } +}