From d3945fe09d856dc0ea0e25a81ebc6ba3efa2e94d Mon Sep 17 00:00:00 2001 From: Dmitry Shtukenberg Date: Wed, 5 Nov 2025 01:00:15 +0300 Subject: [PATCH 01/18] Initial release --- Cargo.lock | 131 +++-- Cargo.toml | 1 + modules/tx_validator_phase1/Cargo.toml | 31 + modules/tx_validator_phase1/NOTES.md | 103 ++++ .../src/tx_validator_phase1.rs | 549 ++++++++++++++++++ processes/omnibus/Cargo.toml | 1 + processes/omnibus/omnibus.toml | 2 + processes/omnibus/src/main.rs | 2 + 8 files changed, 769 insertions(+), 51 deletions(-) create mode 100644 modules/tx_validator_phase1/Cargo.toml create mode 100644 modules/tx_validator_phase1/NOTES.md create mode 100644 modules/tx_validator_phase1/src/tx_validator_phase1.rs diff --git a/Cargo.lock b/Cargo.lock index d237f4ca..f80b236e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,6 +431,27 @@ dependencies = [ "tracing", ] +[[package]] +name = "acropolis_module_tx_validator_phase1" +version = "0.1.0" +dependencies = [ + "acropolis_codec", + "acropolis_common", + "anyhow", + "async-trait", + "caryatid_sdk", + "config", + "csv", + "hex", + "pallas 0.33.0", + "serde", + "serde_json", + "serde_with 3.15.1", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "acropolis_module_upstream_chain_fetcher" version = "0.2.0" @@ -491,6 +512,7 @@ dependencies = [ "acropolis_module_spo_state", "acropolis_module_stake_delta_filter", "acropolis_module_tx_unpacker", + "acropolis_module_tx_validator_phase1", "acropolis_module_upstream_chain_fetcher", "acropolis_module_utxo_state", "anyhow", @@ -1510,9 +1532,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.43" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "jobserver", @@ -3221,9 +3243,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -3364,16 +3386,6 @@ dependencies = [ "redox_syscall 0.5.18", ] -[[package]] -name = "libyml" -version = "0.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" -dependencies = [ - "anyhow", - "version_check", -] - [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -3563,20 +3575,20 @@ dependencies = [ [[package]] name = "mithril-build-script" -version = "0.2.26" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78acda10db18d68f1957ba96035e8e6e8771db6c422188786fe1dc30251075cf" +checksum = "6870a7ed29eda6c167531fac54f4473c566afe597dd67f65f93791f39f776f5d" dependencies = [ + "saphyr", "semver", "serde_json", - "serde_yml", ] [[package]] name = "mithril-cardano-node-internal-database" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06cbfca5474ff1cf3b47c6119ceb75589750fa388321aa94b52f88b9927297e" +checksum = "4bc2ac8d0721cb21de4ed1885cb458ac7f2f577e4d725a62a588f2c4ea8f146a" dependencies = [ "anyhow", "async-trait", @@ -3594,9 +3606,9 @@ dependencies = [ [[package]] name = "mithril-client" -version = "0.12.30" +version = "0.12.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ea0f768a0152709275175063e655e8f67b32b6aa4acfb8ebdcfb007efc6c7b6" +checksum = "8d4c164a8f4d1fc64d04ff7b5f2545bbc12d1f53e9310cb712efb70178a24ec2" dependencies = [ "anyhow", "async-recursion", @@ -3623,9 +3635,9 @@ dependencies = [ [[package]] name = "mithril-common" -version = "0.6.17" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f9fe83e0c04b55d4a82281211960db0da978c4ca53d41ec2f16ab7ff62550f" +checksum = "917f3f615ffb76cd22abc95fb184be8eb17fe620e45eee07ba6854071e03e378" dependencies = [ "anyhow", "async-trait", @@ -3663,9 +3675,9 @@ dependencies = [ [[package]] name = "mithril-stm" -version = "0.5.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec6eef4dfe9ee1510251fb9caef76a9882d062552a5b8c27aae84da670ef05f2" +checksum = "69b11b0c9103d7d5d7df962d069aa1333675ab97ec721f229d57acd622366524" dependencies = [ "blake2 0.10.6", "blst", @@ -3988,6 +4000,15 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -5386,9 +5407,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.34" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "log", "once_cell", @@ -5494,6 +5515,29 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "saphyr" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3767dfe8889ebb55a21409df2b6f36e66abfbe1eb92d64ff76ae799d3f91016" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", + "ordered-float", + "saphyr-parser", +] + +[[package]] +name = "saphyr-parser" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb771b59f6b1985d1406325ec28f97cfb14256abcec4fdfb37b36a1766d6af7" +dependencies = [ + "arraydeque", + "hashlink", +] + [[package]] name = "schannel" version = "0.1.28" @@ -5517,9 +5561,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "1317c3bf3e7df961da95b0a56a172a02abead31276215a0497241a7624b487ce" dependencies = [ "dyn-clone", "ref-cast", @@ -5736,7 +5780,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.12.0", "schemars 0.9.0", - "schemars 1.0.4", + "schemars 1.0.5", "serde_core", "serde_json", "serde_with_macros 3.15.1", @@ -5767,21 +5811,6 @@ dependencies = [ "syn 2.0.108", ] -[[package]] -name = "serde_yml" -version = "0.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" -dependencies = [ - "indexmap 2.12.0", - "itoa", - "libyml", - "memchr", - "ryu", - "serde", - "version_check", -] - [[package]] name = "sha1" version = "0.10.6" @@ -6330,9 +6359,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -6633,9 +6662,9 @@ checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] @@ -6889,9 +6918,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] diff --git a/Cargo.toml b/Cargo.toml index 035c1edb..30a6174e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "modules/consensus", # Chooses favoured chain across multiple options "modules/chain_store", # Tracks historical information about blocks and TXs "modules/tx_submitter", # Submits TXs to peers + "modules/tx_validator_phase1", # Validates TXs (simple, without script execution) # Process builds "processes/omnibus", # All-inclusive omnibus process diff --git a/modules/tx_validator_phase1/Cargo.toml b/modules/tx_validator_phase1/Cargo.toml new file mode 100644 index 00000000..57f483cc --- /dev/null +++ b/modules/tx_validator_phase1/Cargo.toml @@ -0,0 +1,31 @@ +# Acropolis Governance state module + +[package] +name = "acropolis_module_tx_validator_phase1" +version = "0.1.0" +edition = "2021" +authors = ["Dmitry Shtukenberg "] +description = "Transaction validator, phase 1" +license = "Apache-2.0" + +[dependencies] +acropolis_common = { path = "../../common" } +acropolis_codec = { path = "../../codec" } + +caryatid_sdk = { workspace = true } + +anyhow = { workspace = true } +async-trait = "0.1" +config = { workspace = true } +csv = "1" +hex = { workspace = true } +pallas = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +serde_with = { workspace = true, features = ["base64"] } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { version = "0.3.20", features = ["registry", "env-filter"] } + +[lib] +path = "src/tx_validator_phase1.rs" diff --git a/modules/tx_validator_phase1/NOTES.md b/modules/tx_validator_phase1/NOTES.md new file mode 100644 index 00000000..662cf9a9 --- /dev/null +++ b/modules/tx_validator_phase1/NOTES.md @@ -0,0 +1,103 @@ +Validate transactions phase 1 +============================= + +Haskell sources +--------------- + +1. Transaction validation takes place in ledger, in file +`shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs` + +Validation is performed in rule "PPUP", in function +`utxoInductive` + +The following sub-functions are called there: +``` + {- txttl txb ≥ slot -} + runTest $ validateTimeToLive txBody slot + + {- txins txb ≠ ∅ -} + runTest $ validateInputSetEmptyUTxO txBody + + {- minfee pp tx ≤ txfee txb -} + runTest $ validateFeeTooSmallUTxO pp tx utxo + + {- txins txb ⊆ dom utxo -} + runTest $ validateBadInputsUTxO utxo $ txBody ^. inputsTxBodyL + + netId <- liftSTS $ asks networkId + + {- ∀(_ → (a, _)) ∈ txouts txb, netId a = NetworkId -} + runTest $ validateWrongNetwork netId outputs + + {- ∀(a → ) ∈ txwdrls txb, netId a = NetworkId -} + runTest $ validateWrongNetworkWithdrawal netId txBody + + {- consumed pp utxo txb = produced pp poolParams txb -} + runTest $ validateValueNotConservedUTxO pp utxo certState txBody + + -- process Protocol Parameter Update Proposals + ppup' <- + trans @(EraRule "PPUP" era) $ TRC (PPUPEnv slot pp genDelegs, ppup, txBody ^. updateTxBodyL) + + {- ∀(_ → (_, c)) ∈ txouts txb, c ≥ (minUTxOValue pp) -} + runTest $ validateOutputTooSmallUTxO pp outputs + + {- ∀ ( _ ↦ (a,_)) ∈ txoutstxb, a ∈ Addrbootstrap → bootstrapAttrsSize a ≤ 64 -} + runTest $ validateOutputBootAddrAttrsTooBig outputs + + {- txsize tx ≤ maxTxSize pp -} + runTest $ validateMaxTxSizeUTxO pp tx +``` + +2. Another validation step, UTXOW, rule UTXOW + +``` + -- * Individual validation steps + validateFailedNativeScripts, + validateMissingScripts, + validateVerifiedWits, + validateMetadata, + validateMIRInsufficientGenesisSigs, + validateNeededWitnesses, +``` + +``` +transitionRulesUTXOW = do + (TRC (utxoEnv@(UtxoEnv _ pp certState), u, tx)) <- judgmentContext + + {- (utxo,_,_,_ ) := utxoSt -} + {- witsKeyHashes := { hashKey vk | vk ∈ dom(txwitsVKey txw) } -} + let utxo = utxosUtxo u + witsKeyHashes = witsFromTxWitnesses tx + scriptsProvided = getScriptsProvided utxo tx + + -- check scripts + {- ∀ s ∈ range(txscripts txw) ∩ Scriptnative), runNativeScript s tx -} + + runTestOnSignal $ validateFailedNativeScripts scriptsProvided tx + + {- { s | (_,s) ∈ scriptsNeeded utxo tx} = dom(txscripts txw) -} + let scriptsNeeded = getScriptsNeeded utxo (tx ^. bodyTxL) + runTest $ validateMissingScripts scriptsNeeded scriptsProvided + + -- check VKey witnesses + {- ∀ (vk ↦ σ) ∈ (txwitsVKey txw), V_vk⟦ txbodyHash ⟧_σ -} + runTestOnSignal $ validateVerifiedWits tx + + {- witsVKeyNeeded utxo tx genDelegs ⊆ witsKeyHashes -} + runTest $ validateNeededWitnesses witsKeyHashes certState utxo (tx ^. bodyTxL) + + -- check metadata hash + {- ((adh = ◇) ∧ (ad= ◇)) ∨ (adh = hashAD ad) -} + runTestOnSignal $ validateMetadata pp tx + + -- check genesis keys signatures for instantaneous rewards certificates + {- genSig := { hashKey gkey | gkey ∈ dom(genDelegs)} ∩ witsKeyHashes -} + {- { c ∈ txcerts txb ∩ TxCert_mir} ≠ ∅ ⇒ (|genSig| ≥ Quorum) ∧ (d pp > 0) -} + let genDelegs = dsGenDelegs (certState ^. certDStateL) + coreNodeQuorum <- liftSTS $ asks quorum + runTest $ + validateMIRInsufficientGenesisSigs genDelegs coreNodeQuorum witsKeyHashes tx + + trans @(EraRule "UTXO" era) $ TRC (utxoEnv, u, tx) +``` diff --git a/modules/tx_validator_phase1/src/tx_validator_phase1.rs b/modules/tx_validator_phase1/src/tx_validator_phase1.rs new file mode 100644 index 00000000..cb0c137d --- /dev/null +++ b/modules/tx_validator_phase1/src/tx_validator_phase1.rs @@ -0,0 +1,549 @@ +//! Acropolis transaction unpacker module for Caryatid +//! Unpacks transaction bodies into UTXO events + +use acropolis_codec::*; +use acropolis_common::{ + messages::{ + AssetDeltasMessage, BlockTxsMessage, CardanoMessage, GovernanceProceduresMessage, Message, + TxCertificatesMessage, UTXODeltasMessage, WithdrawalsMessage, + }, + *, +}; + +use caryatid_sdk::{module, Context, Module}; +use std::{clone::Clone, fmt::Debug, sync::Arc}; + +use anyhow::Result; +use config::Config; +use pallas::codec::minicbor::encode; +use pallas::ledger::primitives::KeyValuePairs; +use pallas::ledger::{primitives, traverse, traverse::MultiEraTx}; +use tracing::{debug, error, info, info_span, Instrument}; + +//mod utxo_registry; +//use crate::utxo_registry::UTxORegistry; + +const DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC: &str = "cardano.txs"; +const DEFAULT_GENESIS_SUBSCRIBE_TOPIC: &str = "cardano.genesis.utxos"; + +//const CIP25_METADATA_LABEL: u64 = 721; + +/// Tx unpacker module +/// Parameterised by the outer message enum used on the bus +#[module( + message_type(Message), + name = "tx-validator-phase1", + description = "Transactions validator, Phase 1" +)] +pub struct TxValidatorPhase1; + +pub struct State { +} + +impl State { + pub async fn run() -> Result<()> { + info!("Validation started!"); + Ok(()) + } +} + +impl TxValidatorPhase1 { + /// Main init function + pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { + // Get configuration + let transactions_subscribe_topic = config + .get_string("subscribe-topic") + .unwrap_or(DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC.to_string()); + info!("Creating subscriber on '{transactions_subscribe_topic}'"); + + let genesis_utxos_subscribe_topic = config + .get_string("genesis-utxos-subscribe-topic") + .unwrap_or(DEFAULT_GENESIS_SUBSCRIBE_TOPIC.to_string()); + info!("Creating subscriber on '{genesis_utxos_subscribe_topic}'"); + + let publish_validation_result = config.get_string("publish-validation-result-phase1").ok(); + if let Some(ref topic) = publish_validation_result { + info!("Publishing UTXO deltas on '{topic}'"); + } + let network_id: NetworkId = + config.get_string("network-id").unwrap_or("mainnet".to_string()).into(); + + // Initialize UTxORegistry + //let mut utxo_registry = UTxORegistry::default(); + + // Subscribe to genesis and txs topics + let mut genesis_sub = context.subscribe(&genesis_utxos_subscribe_topic).await?; + let mut txs_sub = context.subscribe(&transactions_subscribe_topic).await?; + + context.clone().run(async move { + State::run().await.unwrap_or_else(|e| error!("TX validator failed: {e}")); + }); + + Ok(()) + } +} + +/* + let publish_asset_deltas_topic = config.get_string("publish-asset-deltas-topic").ok(); + if let Some(ref topic) = publish_asset_deltas_topic { + info!("Publishing native asset deltas on '{topic}'"); + } + + let publish_withdrawals_topic = config.get_string("publish-withdrawals-topic").ok(); + if let Some(ref topic) = publish_withdrawals_topic { + info!("Publishing withdrawals on '{topic}'"); + } + + let publish_certificates_topic = config.get_string("publish-certificates-topic").ok(); + if let Some(ref topic) = publish_certificates_topic { + info!("Publishing certificates on '{topic}'"); + } + + let publish_governance_procedures_topic = + config.get_string("publish-governance-topic").ok(); + if let Some(ref topic) = publish_governance_procedures_topic { + info!("Publishing governance procedures on '{topic}'"); + } + + let publish_block_txs_topic = config.get_string("publish-block-txs-topic").ok(); + if let Some(ref topic) = publish_block_txs_topic { + info!("Publishing block txs on '{topic}'"); + } +*/ + +/* + fn decode_updates( + dest: &mut Vec, + proposals: &KeyValuePairs, + epoch: u64, + map: impl Fn(&EraSpecificUpdateProposals) -> Result>, + ) { + let mut update = AlonzoBabbageUpdateProposal { + proposals: Vec::new(), + enactment_epoch: epoch, + }; + + for (hash_bytes, vote) in proposals.iter() { + let hash = match GenesisKeyhash::try_from(hash_bytes.as_ref()) { + Ok(h) => h, + Err(e) => { + error!("Invalid genesis keyhash in protocol parameter update: {e}"); + continue; + } + }; + + match map(vote) { + Ok(upd) => update.proposals.push((hash, upd)), + Err(e) => error!("Cannot convert protocol param update {vote:?}: {e}"), + } + } + + dest.push(update); + } + /// Main init function + pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { + // Get configuration + let transactions_subscribe_topic = config + .get_string("subscribe-topic") + .unwrap_or(DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC.to_string()); + info!("Creating subscriber on '{transactions_subscribe_topic}'"); + + let genesis_utxos_subscribe_topic = config + .get_string("genesis-utxos-subscribe-topic") + .unwrap_or(DEFAULT_GENESIS_SUBSCRIBE_TOPIC.to_string()); + info!("Creating subscriber on '{genesis_utxos_subscribe_topic}'"); + + let publish_utxo_deltas_topic = config.get_string("publish-utxo-deltas-topic").ok(); + if let Some(ref topic) = publish_utxo_deltas_topic { + info!("Publishing UTXO deltas on '{topic}'"); + } + + let publish_asset_deltas_topic = config.get_string("publish-asset-deltas-topic").ok(); + if let Some(ref topic) = publish_asset_deltas_topic { + info!("Publishing native asset deltas on '{topic}'"); + } + + let publish_withdrawals_topic = config.get_string("publish-withdrawals-topic").ok(); + if let Some(ref topic) = publish_withdrawals_topic { + info!("Publishing withdrawals on '{topic}'"); + } + + let publish_certificates_topic = config.get_string("publish-certificates-topic").ok(); + if let Some(ref topic) = publish_certificates_topic { + info!("Publishing certificates on '{topic}'"); + } + + let publish_governance_procedures_topic = + config.get_string("publish-governance-topic").ok(); + if let Some(ref topic) = publish_governance_procedures_topic { + info!("Publishing governance procedures on '{topic}'"); + } + + let publish_block_txs_topic = config.get_string("publish-block-txs-topic").ok(); + if let Some(ref topic) = publish_block_txs_topic { + info!("Publishing block txs on '{topic}'"); + } + + let network_id: NetworkId = + config.get_string("network-id").unwrap_or("mainnet".to_string()).into(); + + // Initialize UTxORegistry + let mut utxo_registry = UTxORegistry::default(); + + // Subscribe to genesis and txs topics + let mut genesis_sub = context.subscribe(&genesis_utxos_subscribe_topic).await?; + let mut txs_sub = context.subscribe(&transactions_subscribe_topic).await?; + + context.clone().run(async move { + // Initialize TxRegistry with genesis utxos + let (_, message) = genesis_sub.read().await + .expect("failed to read genesis utxos"); + match message.as_ref() { + Message::Cardano((_block, CardanoMessage::GenesisUTxOs(genesis_msg))) => { + utxo_registry.bootstrap_from_genesis_utxos(&genesis_msg.utxos); + info!("Seeded registry with {} genesis utxos", genesis_msg.utxos.len()); + } + other => panic!("expected GenesisUTxOs, got {:?}", other), + } + loop { + let Ok((_, message)) = txs_sub.read().await else { return; }; + match message.as_ref() { + Message::Cardano((block, CardanoMessage::ReceivedTxs(txs_msg))) => { + let span = info_span!("tx_unpacker.run", block = block.number); + + async { + if tracing::enabled!(tracing::Level::DEBUG) { + debug!("Received {} txs for slot {}", + txs_msg.txs.len(), block.slot); + } + + let mut utxo_deltas = Vec::new(); + let mut asset_deltas = Vec::new(); + let mut cip25_metadata_updates = Vec::new(); + let mut withdrawals = Vec::new(); + let mut certificates = Vec::new(); + let mut voting_procedures = Vec::new(); + let mut proposal_procedures = Vec::new(); + let mut alonzo_babbage_update_proposals = Vec::new(); + let mut total_output: u128 = 0; + let mut total_fees: u64 = 0; + let total_txs = txs_msg.txs.len() as u64; + + // handle rollback or advance registry to the next block + let block_number = block.number as u32; + if block.status == BlockStatus::RolledBack { + if let Err(e) = utxo_registry.rollback_before(block_number) { + error!("rollback_before({}) failed: {}", block_number, e); + } + utxo_registry.next_block(); + } + + for (tx_index , raw_tx) in txs_msg.txs.iter().enumerate() { + let tx_index = tx_index as u16; + + // Parse the tx + match MultiEraTx::decode(raw_tx) { + Ok(tx) => { + let tx_hash: TxHash = tx.hash().to_vec().try_into().expect("invalid tx hash length"); + let tx_identifier = TxIdentifier::new(block_number, tx_index); + + let inputs = tx.consumes(); + let outputs = tx.produces(); + let certs = tx.certs(); + let tx_withdrawals = tx.withdrawals_sorted_set(); + let mut props = None; + let mut votes = None; + + if tracing::enabled!(tracing::Level::DEBUG) { + debug!("Decoded tx with {} inputs, {} outputs, {} certs", + inputs.len(), outputs.len(), certs.len()); + } + + if publish_utxo_deltas_topic.is_some() { + // Add all the inputs + for input in inputs { // MultiEraInput + // Lookup and remove UTxOIdentifier from registry + let oref = input.output_ref(); + let tx_ref = TxOutRef::new(TxHash::from(**oref.hash()), oref.index() as u16); + + match utxo_registry.consume(&tx_ref) { + Ok(tx_identifier) => { + // Add TxInput to utxo_deltas + utxo_deltas.push(UTXODelta::Input(TxInput { + utxo_identifier: UTxOIdentifier::new( + tx_identifier.block_number(), + tx_identifier.tx_index(), + tx_ref.output_index, + ), + })); + } + Err(e) => { + error!("Failed to consume input {}: {e}", tx_ref.output_index); + } + } + } + + // Add all the outputs + for (index, output) in outputs { + // Add TxOutRef to registry + match utxo_registry.add( + block_number, + tx_index, + TxOutRef { + tx_hash, + output_index: index as u16, + }, + ) { + Ok(utxo_id) => { + match output.address() { + Ok(pallas_address) => match map_parameters::map_address(&pallas_address) { + Ok(address) => { + // Add TxOutput to utxo_deltas + utxo_deltas.push(UTXODelta::Output(TxOutput { + utxo_identifier: utxo_id, + address, + value: map_parameters::map_value(&output.value()), + datum: map_parameters::map_datum(&output.datum()), + })); + + // catch all output lovelaces + total_output += output.value().coin() as u128; + } + Err(e) => error!("Output {index} in tx ignored: {e}"), + }, + Err(e) => error!("Can't parse output {index} in tx: {e}"), + } + } + Err(e) => { + error!("Failed to insert output into registry: {e}"); + } + } + } + } + + if publish_asset_deltas_topic.is_some() { + let mut tx_deltas: Vec<(PolicyId, Vec)> = Vec::new(); + + // Mint deltas + for policy_group in tx.mints().iter() { + if let Some((policy_id, deltas)) = map_parameters::map_mint_burn(policy_group) { + tx_deltas.push((policy_id, deltas)); + } + } + + if let Some(metadata) = tx.metadata().find(CIP25_METADATA_LABEL) { + let mut metadata_raw = Vec::new(); + match encode(metadata, &mut metadata_raw) { + Ok(()) => { + cip25_metadata_updates.push(metadata_raw); + } + Err(e) => { + error!("failed to encode CIP-25 metadatum: {e:#}"); + } + } + } + + if !tx_deltas.is_empty() { + asset_deltas.push((tx_identifier, tx_deltas)); + } + } + + if publish_certificates_topic.is_some() { + for ( cert_index, cert) in certs.iter().enumerate() { + match map_parameters::map_certificate(cert, tx_identifier, cert_index, network_id.clone()) { + Ok(tx_cert) => { + certificates.push(tx_cert); + }, + Err(_e) => { + // TODO error unexpected + //error!("{e}"); + } + } + } + } + + if publish_withdrawals_topic.is_some() { + for (key, value) in tx_withdrawals { + match StakeAddress::from_binary(key) { + Ok(stake_address) => { + withdrawals.push(Withdrawal { + address: stake_address, + value, + tx_identifier + }); + } + Err(e) => error!("Bad stake address: {e:#}"), + } + } + } + + if publish_governance_procedures_topic.is_some() { + //Self::decode_legacy_updates(&mut legacy_update_proposals, &block, &raw_tx); + if block.era >= Era::Shelley && block.era < Era::Babbage { + if let Ok(alonzo) = MultiEraTx::decode_for_era(traverse::Era::Alonzo, raw_tx) { + if let Some(update) = alonzo.update() { + if let Some(alonzo_update) = update.as_alonzo() { + Self::decode_updates( + &mut alonzo_babbage_update_proposals, + &alonzo_update.proposed_protocol_parameter_updates, + alonzo_update.epoch, + map_parameters::map_alonzo_protocol_param_update + ); + } + } + } + } + else if block.era >= Era::Babbage && block.era < Era::Conway{ + if let Ok(babbage) = MultiEraTx::decode_for_era(traverse::Era::Babbage, raw_tx) { + if let Some(update) = babbage.update() { + if let Some(babbage_update) = update.as_babbage() { + Self::decode_updates( + &mut alonzo_babbage_update_proposals, + &babbage_update.proposed_protocol_parameter_updates, + babbage_update.epoch, + map_parameters::map_babbage_protocol_param_update + ); + } + } + } + } + } + + if let Some(conway) = tx.as_conway() { + if let Some(ref v) = conway.transaction_body.voting_procedures { + votes = Some(v); + } + + if let Some(ref p) = conway.transaction_body.proposal_procedures { + props = Some(p); + } + } + + + if publish_governance_procedures_topic.is_some() { + if let Some(pp) = props { + // Nonempty set -- governance_message.proposal_procedures will not be empty + let mut proc_id = GovActionId { transaction_id: tx_hash, action_index: 0 }; + for (action_index, pallas_governance_proposals) in pp.iter().enumerate() { + match proc_id.set_action_index(action_index) + .and_then (|proc_id| map_parameters::map_governance_proposals_procedures(proc_id, pallas_governance_proposals)) + { + Ok(g) => proposal_procedures.push(g), + Err(e) => error!("Cannot decode governance proposal procedure {} idx {} in slot {}: {e}", proc_id, action_index, block.slot) + } + } + } + + if let Some(pallas_vp) = votes { + // Nonempty set -- governance_message.voting_procedures will not be empty + match map_parameters::map_all_governance_voting_procedures(pallas_vp) { + Ok(vp) => voting_procedures.push((tx_hash, vp)), + Err(e) => error!("Cannot decode governance voting procedures in slot {}: {e}", block.slot) + } + } + } + + // Capture the fees + if let Some(fee) = tx.fee() { + total_fees += fee; + } + }, + + Err(e) => error!("Can't decode transaction in slot {}: {e}", + block.slot) + } + } + + utxo_registry.next_block(); + + // Publish messages in parallel + let mut futures = Vec::new(); + if let Some(ref topic) = publish_utxo_deltas_topic { + let msg = Message::Cardano(( + block.clone(), + CardanoMessage::UTXODeltas(UTXODeltasMessage { + deltas: utxo_deltas, + }) + )); + + futures.push(context.message_bus.publish(topic, Arc::new(msg))); + } + + if let Some(ref topic) = publish_asset_deltas_topic { + let msg = Message::Cardano(( + block.clone(), + CardanoMessage::AssetDeltas(AssetDeltasMessage { + deltas: asset_deltas, + cip25_metadata_updates + }) + )); + + futures.push(context.message_bus.publish(topic, Arc::new(msg))); + } + + if let Some(ref topic) = publish_withdrawals_topic { + let msg = Message::Cardano(( + block.clone(), + CardanoMessage::Withdrawals(WithdrawalsMessage { + withdrawals, + }) + )); + + futures.push(context.message_bus.publish(topic, Arc::new(msg))); + } + + if let Some(ref topic) = publish_certificates_topic { + let msg = Message::Cardano(( + block.clone(), + CardanoMessage::TxCertificates(TxCertificatesMessage { + certificates, + }) + )); + + futures.push(context.message_bus.publish(topic, Arc::new(msg))); + } + + if let Some(ref topic) = publish_governance_procedures_topic { + let governance_msg = Arc::new(Message::Cardano(( + block.clone(), + CardanoMessage::GovernanceProcedures( + GovernanceProceduresMessage { + voting_procedures, + proposal_procedures, + alonzo_babbage_updates: alonzo_babbage_update_proposals + }) + ))); + + futures.push(context.message_bus.publish(topic, + governance_msg.clone())); + } + + if let Some(ref topic) = publish_block_txs_topic { + let msg = Message::Cardano(( + block.clone(), + CardanoMessage::BlockInfoMessage(BlockTxsMessage { + total_txs, + total_output, + total_fees + }) + )); + + futures.push(context.message_bus.publish(topic, Arc::new(msg))); + } + + join_all(futures) + .await + .into_iter() + .filter_map(Result::err) + .for_each(|e| error!("Failed to publish: {e}")); + }.instrument(span).await; + } + + _ => error!("Unexpected message type: {message:?}") + } + } + }); + + Ok(()) + } +*/ diff --git a/processes/omnibus/Cargo.toml b/processes/omnibus/Cargo.toml index 7814b5c8..621664b5 100644 --- a/processes/omnibus/Cargo.toml +++ b/processes/omnibus/Cargo.toml @@ -30,6 +30,7 @@ acropolis_module_chain_store = { path = "../../modules/chain_store" } acropolis_module_address_state = { path = "../../modules/address_state" } acropolis_module_consensus = { path = "../../modules/consensus" } acropolis_module_historical_accounts_state = { path = "../../modules/historical_accounts_state" } +acropolis_module_tx_validator_phase1 = { path = "../../modules/tx_validator_phase1" } caryatid_process = { workspace = true } caryatid_sdk = { workspace = true } diff --git a/processes/omnibus/omnibus.toml b/processes/omnibus/omnibus.toml index 6922217e..f4294fdf 100644 --- a/processes/omnibus/omnibus.toml +++ b/processes/omnibus/omnibus.toml @@ -137,6 +137,8 @@ store-totals = false # Enables /addresses/{address}/transactions endpoint store-transactions = false +[module.tx-validator-phase1] + [module.clock] [module.rest-server] diff --git a/processes/omnibus/src/main.rs b/processes/omnibus/src/main.rs index ab2d4ca2..579f2412 100644 --- a/processes/omnibus/src/main.rs +++ b/processes/omnibus/src/main.rs @@ -29,6 +29,7 @@ use acropolis_module_stake_delta_filter::StakeDeltaFilter; use acropolis_module_tx_unpacker::TxUnpacker; use acropolis_module_upstream_chain_fetcher::UpstreamChainFetcher; use acropolis_module_utxo_state::UTXOState; +use acropolis_module_tx_validator_phase1::TxValidatorPhase1; use caryatid_module_clock::Clock; use caryatid_module_rest_server::RESTServer; @@ -119,6 +120,7 @@ pub async fn main() -> Result<()> { DRDDState::register(&mut process); Consensus::register(&mut process); ChainStore::register(&mut process); + TxValidatorPhase1::register(&mut process); Clock::::register(&mut process); RESTServer::::register(&mut process); From f409f665d79af942f9f4d913faaff66367b0efa9 Mon Sep 17 00:00:00 2001 From: Dmitry Shtukenberg Date: Fri, 7 Nov 2025 17:40:05 +0300 Subject: [PATCH 02/18] Elementary working pass --- Cargo.lock | 584 +++++++++-------- modules/tx_validator_phase1/NOTES.md | 78 ++- modules/tx_validator_phase1/src/state.rs | 31 + .../src/tx_validator_phase1.rs | 596 +++--------------- 4 files changed, 540 insertions(+), 749 deletions(-) create mode 100644 modules/tx_validator_phase1/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index f80b236e..e5651bd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -692,46 +692,50 @@ dependencies = [ [[package]] name = "amq-protocol" -version = "7.2.3" +version = "8.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "587d313f3a8b4a40f866cc84b6059fe83133bf172165ac3b583129dd211d8e1c" +checksum = "355603365d2217f7fbc03f0be085ea1440498957890f04276402012cdde445f5" dependencies = [ "amq-protocol-tcp", "amq-protocol-types", "amq-protocol-uri", "cookie-factory", - "nom 7.1.3", + "nom 8.0.0", "serde", ] [[package]] name = "amq-protocol-tcp" -version = "7.2.3" +version = "8.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc707ab9aa964a85d9fc25908a3fdc486d2e619406883b3105b48bf304a8d606" +checksum = "8d7b97a85e08671697e724a6b7f1459ff81603613695e3151764a9529c6fec15" dependencies = [ "amq-protocol-uri", + "async-trait", + "cfg-if", + "executor-trait 2.1.2", + "reactor-trait 2.8.0", "tcp-stream", "tracing", ] [[package]] name = "amq-protocol-types" -version = "7.2.3" +version = "8.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf99351d92a161c61ec6ecb213bc7057f5b837dd4e64ba6cb6491358efd770c4" +checksum = "a2984a816dba991b5922503921d8f94650792bdeac47c27c83830710d2567f63" dependencies = [ "cookie-factory", - "nom 7.1.3", + "nom 8.0.0", "serde", "serde_json", ] [[package]] name = "amq-protocol-uri" -version = "7.2.3" +version = "8.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89f8273826a676282208e5af38461a07fe939def57396af6ad5997fcf56577d" +checksum = "f31db8e69d1456ec8ecf6ee598707179cf1d95f34f7d30037b16ad43f0cddcff" dependencies = [ "amq-protocol-types", "percent-encoding", @@ -845,7 +849,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", "synstructure", ] @@ -857,7 +861,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -893,8 +897,8 @@ checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.3.0", - "futures-lite 2.6.1", + "fastrand", + "futures-lite", "pin-project-lite", "slab", ] @@ -907,10 +911,10 @@ checksum = "13f937e26114b93193065fd44f507aa2e9169ad0cdabbb996920b1fe1ddea7ba" dependencies = [ "async-channel", "async-executor", - "async-io 2.6.0", - "async-lock 3.4.1", + "async-io", + "async-lock", "blocking", - "futures-lite 2.6.1", + "futures-lite", ] [[package]] @@ -921,27 +925,20 @@ checksum = "9af57045d58eeb1f7060e7025a1631cbc6399e0a1d10ad6735b3d0ea7f8346ce" dependencies = [ "async-global-executor", "async-trait", - "executor-trait", + "executor-trait 2.1.2", ] [[package]] -name = "async-io" -version = "1.13.0" +name = "async-global-executor-trait" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +checksum = "c3727b7da74b92d2d03403cf1142706b53423e5c050791af438f8f50edea057a" dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.28", - "slab", - "socket2 0.4.10", - "waker-fn", + "async-global-executor", + "async-global-executor-trait 2.2.0", + "async-trait", + "executor-trait 2.1.2", + "executor-trait 3.1.0", ] [[package]] @@ -954,44 +951,35 @@ dependencies = [ "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.6.1", + "futures-lite", "parking", - "polling 3.11.0", - "rustix 1.1.2", + "polling", + "rustix", "slab", "windows-sys 0.61.2", ] -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - [[package]] name = "async-lock" version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ - "event-listener 5.4.1", + "event-listener", "event-listener-strategy", "pin-project-lite", ] [[package]] name = "async-reactor-trait" -version = "1.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6012d170ad00de56c9ee354aef2e358359deb1ec504254e0e5a3774771de0e" +checksum = "8ab52004af1f14a170088bd9e10a2d3b2f2307ce04320e58a6ce36ee531be625" dependencies = [ - "async-io 1.13.0", + "async-io", "async-trait", "futures-core", - "reactor-trait", + "reactor-trait 3.1.1", ] [[package]] @@ -1002,7 +990,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1019,7 +1007,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1034,6 +1022,29 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" version = "0.6.20" @@ -1089,6 +1100,15 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", +] + [[package]] name = "base58" version = "0.2.0" @@ -1174,6 +1194,26 @@ dependencies = [ "virtue", ] +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.109", +] + [[package]] name = "bip39" version = "2.2.0" @@ -1230,7 +1270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6cbbb8f56245b5a479b30a62cdc86d26e2f35c2b9f594bc4671654b03851380" dependencies = [ "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1292,7 +1332,7 @@ dependencies = [ "async-channel", "async-task", "futures-io", - "futures-lite 2.6.1", + "futures-lite", "piper", ] @@ -1328,7 +1368,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1482,9 +1522,9 @@ dependencies = [ [[package]] name = "caryatid_process" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295eb5e868e9f1a9d2eebfd1d883b5cb7d9f857d56a3b2542f33f15dfd44563e" +checksum = "1b75c2e960f783c2dc3879d632aa4643f2e21b41572313e7e22b878ecc8b4642" dependencies = [ "anyhow", "async-trait", @@ -1492,11 +1532,11 @@ dependencies = [ "config", "futures", "lapin", + "minicbor-serde", "serde", - "serde_cbor", "serde_json", "tokio", - "tokio-executor-trait", + "tokio-executor-trait 3.1.0", "tracing", "tracing-subscriber", ] @@ -1532,9 +1572,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.44" +version = "1.2.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" +checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe" dependencies = [ "find-msvc-tools", "jobserver", @@ -1542,6 +1582,15 @@ dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -1614,6 +1663,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.51" @@ -1645,7 +1705,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -1654,6 +1714,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "cms" version = "0.2.3" @@ -1778,6 +1847,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -1966,7 +2045,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2000,7 +2079,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2014,7 +2093,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2025,7 +2104,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2036,7 +2115,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2094,7 +2173,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2144,7 +2223,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2168,6 +2247,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0d05e1c0dbad51b52c38bda7adceef61b9efc2baf04acfe8726a8c4630a6f57" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -2233,7 +2318,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -2253,9 +2338,9 @@ dependencies = [ [[package]] name = "erased-serde" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" dependencies = [ "serde", "serde_core", @@ -2272,12 +2357,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - [[package]] name = "event-listener" version = "5.4.1" @@ -2295,7 +2374,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener 5.4.1", + "event-listener", "pin-project-lite", ] @@ -2309,12 +2388,12 @@ dependencies = [ ] [[package]] -name = "fastrand" -version = "1.9.0" +name = "executor-trait" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +checksum = "57d6a1fc6700fa12782770cb344a29172ae940ea41d5fd5049fdf236dd6eaa92" dependencies = [ - "instant", + "async-trait", ] [[package]] @@ -2466,6 +2545,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -2520,28 +2605,13 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - [[package]] name = "futures-lite" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ - "fastrand 2.3.0", + "fastrand", "futures-core", "futures-io", "parking", @@ -2556,7 +2626,18 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", +] + +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls", + "rustls-pki-types", ] [[package]] @@ -2776,12 +2857,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hermit-abi" version = "0.5.2" @@ -3224,17 +3299,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -3328,24 +3392,21 @@ dependencies = [ [[package]] name = "lapin" -version = "2.5.5" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d2aa4725b9607915fa1a73e940710a3be6af508ce700e56897cbe8847fbb07" +checksum = "913a84142a99160ecef997a5c17c53639bcbac4424a0315a5ffe6c8be8e8db86" dependencies = [ "amq-protocol", - "async-global-executor-trait", + "async-global-executor-trait 3.1.0", "async-reactor-trait", "async-trait", - "executor-trait", + "backon", + "executor-trait 2.1.2", "flume", "futures-core", "futures-io", - "parking_lot 0.12.5", - "pinky-swear", - "reactor-trait", - "serde", + "reactor-trait 2.8.0", "tracing", - "waker-fn", ] [[package]] @@ -3369,6 +3430,16 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libm" version = "0.2.15" @@ -3386,12 +3457,6 @@ dependencies = [ "redox_syscall 0.5.18", ] -[[package]] -name = "linux-raw-sys" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -3524,6 +3589,15 @@ dependencies = [ "minicbor-derive 0.16.2", ] +[[package]] +name = "minicbor" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f182275033b808ede9427884caa8e05fa7db930801759524ca7925bd8aa7a82" +dependencies = [ + "minicbor-derive 0.18.2", +] + [[package]] name = "minicbor-derive" version = "0.15.3" @@ -3532,7 +3606,7 @@ checksum = "bd2209fff77f705b00c737016a48e73733d7fbccb8b007194db148f03561fb70" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -3543,7 +3617,28 @@ checksum = "a9882ef5c56df184b8ffc107fc6c61e33ee3a654b021961d790a78571bb9d67a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", +] + +[[package]] +name = "minicbor-derive" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b17290c95158a760027059fe3f511970d6857e47ff5008f9e09bffe3d3e1c6af" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.109", +] + +[[package]] +name = "minicbor-serde" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "546cc904f35809921fa57016a84c97e68d9d27c012e87b9dadc28c233705f783" +dependencies = [ + "minicbor 2.1.1", + "serde", ] [[package]] @@ -3719,7 +3814,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -3838,7 +3933,7 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.5.2", + "hermit-abi", "libc", ] @@ -3892,7 +3987,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4021,10 +4116,11 @@ dependencies = [ [[package]] name = "p12-keystore" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cae83056e7cb770211494a0ecf66d9fa7eba7d00977e5bb91f0e925b40b937f" +checksum = "e8d55319bae67f92141ce4da80c5392acd3d1323bd8312c1ffdfb018927d07d7" dependencies = [ + "base64 0.22.1", "cbc", "cms", "der", @@ -4618,7 +4714,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4658,7 +4754,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4673,18 +4769,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pinky-swear" -version = "6.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1ea6e230dd3a64d61bcb8b79e597d3ab6b4c94ec7a234ce687dd718b4f2e657" -dependencies = [ - "doc-comment", - "flume", - "parking_lot 0.12.5", - "tracing", -] - [[package]] name = "piper" version = "0.2.4" @@ -4692,7 +4776,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", - "fastrand 2.3.0", + "fastrand", "futures-io", ] @@ -4742,22 +4826,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - [[package]] name = "polling" version = "3.11.0" @@ -4766,9 +4834,9 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.5.2", + "hermit-abi", "pin-project-lite", - "rustix 1.1.2", + "rustix", "windows-sys 0.61.2", ] @@ -4803,7 +4871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4850,7 +4918,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.108", + "syn 2.0.109", "tempfile", ] @@ -4864,7 +4932,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -4963,9 +5031,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -5087,11 +5155,24 @@ dependencies = [ [[package]] name = "reactor-trait" -version = "1.1.0" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffbbf16bc3e4db5fdcf4b77cebf1313610b54b339712aa90088d2d9b1acb1f1" +dependencies = [ + "async-trait", + "reactor-trait 3.1.1", +] + +[[package]] +name = "reactor-trait" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "438a4293e4d097556730f4711998189416232f009c137389e0f961d2bc0ddc58" +checksum = "5b1c85237926dd82e8bc3634240ecf2236ea81e904b3d83cdb1df974af9af293" dependencies = [ + "async-io", "async-trait", + "executor-trait 2.1.2", + "flume", "futures-core", "futures-io", ] @@ -5131,7 +5212,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -5378,20 +5459,6 @@ dependencies = [ "nom 7.1.3", ] -[[package]] -name = "rustix" -version = "0.37.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - [[package]] name = "rustix" version = "1.1.2" @@ -5401,7 +5468,7 @@ dependencies = [ "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys", "windows-sys 0.61.2", ] @@ -5411,6 +5478,7 @@ version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", @@ -5422,10 +5490,12 @@ dependencies = [ [[package]] name = "rustls-connector" -version = "0.20.2" +version = "0.21.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70cc376c6ba1823ae229bacf8ad93c136d93524eab0e4e5e0e4f96b9c4e5b212" +checksum = "10eb7ce243317e6b6a342ef6bff8c2e0d46d78120a9aeb2ee39693a569615c96" dependencies = [ + "futures-io", + "futures-rustls", "log", "rustls", "rustls-native-certs", @@ -5435,15 +5505,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.7.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", - "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.5.1", ] [[package]] @@ -5480,6 +5549,7 @@ version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -5561,9 +5631,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.5" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1317c3bf3e7df961da95b0a56a172a02abead31276215a0497241a7624b487ce" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -5607,7 +5677,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -5651,7 +5734,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" dependencies = [ - "erased-serde 0.4.8", + "erased-serde 0.4.9", "serde", "serde_core", "typeid", @@ -5694,7 +5777,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -5780,7 +5863,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.12.0", "schemars 0.9.0", - "schemars 1.0.5", + "schemars 1.1.0", "serde_core", "serde_json", "serde_with_macros 3.15.1", @@ -5796,7 +5879,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -5808,7 +5891,7 @@ dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -5918,16 +6001,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "socket2" version = "0.5.10" @@ -6003,7 +6076,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -6025,9 +6098,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.108" +version = "2.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f" dependencies = [ "proc-macro2", "quote", @@ -6057,7 +6130,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -6067,7 +6140,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.5.0", ] @@ -6078,7 +6151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -6121,12 +6194,14 @@ dependencies = [ [[package]] name = "tcp-stream" -version = "0.28.0" +version = "0.30.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495b0abdce3dc1f8fd27240651c9e68890c14e9d9c61527b1ce44d8a5a7bd3d5" +checksum = "282ebecea8280bce8b7a0695b5dc93a19839dd445cbba70d3e07c9f6e12c4653" dependencies = [ "cfg-if", + "futures-io", "p12-keystore", + "reactor-trait 2.8.0", "rustls-connector", "rustls-pemfile 2.2.0", ] @@ -6137,10 +6212,10 @@ version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ - "fastrand 2.3.0", + "fastrand", "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", + "rustix", "windows-sys 0.61.2", ] @@ -6170,7 +6245,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -6181,7 +6256,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -6311,8 +6386,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6278565f9fd60c2d205dfbc827e8bb1236c2b1a57148708e95861eff7a6b3bad" dependencies = [ "async-trait", - "executor-trait", + "executor-trait 2.1.2", + "tokio", +] + +[[package]] +name = "tokio-executor-trait" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7fe39b60ffe238db070f2e66fab42c3c2967482e2fbf754afd7e65525cf0da" +dependencies = [ + "async-trait", + "executor-trait 2.1.2", + "executor-trait 3.1.0", "tokio", + "tokio-executor-trait 2.4.0", ] [[package]] @@ -6323,7 +6411,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -6546,7 +6634,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -6630,7 +6718,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be2212c8a9b9bcfca32024de14998494cf9a5dfa59ea1b829de98bac374b86bf" dependencies = [ - "erased-serde 0.4.8", + "erased-serde 0.4.9", "inventory", "once_cell", "serde", @@ -6645,7 +6733,7 @@ checksum = "27a7a9b72ba121f6f1f6c3632b85604cac41aedb5ddc70accbebb6cac83de846" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -6785,12 +6873,6 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - [[package]] name = "walkdir" version = "2.5.0" @@ -6870,7 +6952,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", "wasm-bindgen-shared", ] @@ -6986,7 +7068,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -6997,7 +7079,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -7356,7 +7438,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.2", + "rustix", ] [[package]] @@ -7395,7 +7477,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", "synstructure", ] @@ -7416,7 +7498,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -7436,7 +7518,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", "synstructure", ] @@ -7457,7 +7539,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] @@ -7490,7 +7572,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.108", + "syn 2.0.109", ] [[package]] diff --git a/modules/tx_validator_phase1/NOTES.md b/modules/tx_validator_phase1/NOTES.md index 662cf9a9..e206e62f 100644 --- a/modules/tx_validator_phase1/NOTES.md +++ b/modules/tx_validator_phase1/NOTES.md @@ -1,15 +1,33 @@ Validate transactions phase 1 ============================= -Haskell sources ---------------- +Haskell sources. Shelley epoch UTxO rule +---------------------------------------- 1. Transaction validation takes place in ledger, in file `shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs` -Validation is performed in rule "PPUP", in function +Validation is performed in rule "UTXO" ("PPUP"), in function `utxoInductive` +This is the context of validation rules: +``` + TRC (UtxoEnv slot pp certState, utxos, tx) <- judgmentContext + let utxo = utxos ^. utxoL + UTxOState _ _ _ ppup _ _ = utxos + txBody = tx ^. bodyTxL + outputs = txBody ^. outputsTxBodyL + genDelegs = dsGenDelegs (certState ^. certDStateL) + netId <- liftSTS $ asks networkId + -- process Protocol Parameter Update Proposals + ppup' <- + trans @(EraRule "PPUP" era) $ TRC (PPUPEnv slot pp genDelegs, ppup, txBody ^. updateTxBodyL) +``` + +* Values `utxo`, `ppup`, `genDelegs` require knowledge of `judgementContext`, +that is previous state of Ledger. +* `pp` is parameters state, already sent. + The following sub-functions are called there: ``` {- txttl txb ≥ slot -} @@ -49,7 +67,34 @@ The following sub-functions are called there: runTest $ validateMaxTxSizeUTxO pp tx ``` -2. Another validation step, UTXOW, rule UTXOW +2. The following checks require knowledge of ledger: + + +``` + {- txins txb ⊆ dom utxo -} + runTest $ validateBadInputsUTxO utxo $ txBody ^. inputsTxBodyL +``` + +where in `cardano-ledger-core/src/Cardano/Ledger/TxIn.hs` and +`cardano-ledger-core/src/Cardano/Ledger/State/UTxO.hs`: + +``` +-- | A unique ID of a transaction, which is computable from the transaction. +newtype TxId = TxId {unTxId :: SafeHash EraIndependentTxBody} + deriving (Show, Eq, Ord, Generic) + deriving newtype (NoThunks, ToJSON, FromJSON, HeapWords, EncCBOR, DecCBOR, NFData, MemPack) + +-- | The input of a UTxO. +data TxIn = TxIn !TxId {-# UNPACK #-} !TxIx + deriving (Generic, Eq, Ord, Show) + +-- | The unspent transaction outputs. +newtype UTxO era = UTxO {unUTxO :: Map.Map TxIn (TxOut era)} + deriving (Default, Generic, Semigroup) +``` + +Shelley checks for UTXOW, rule "UTXOW" +-------------------------------------- ``` -- * Individual validation steps @@ -101,3 +146,28 @@ transitionRulesUTXOW = do trans @(EraRule "UTXO" era) $ TRC (utxoEnv, u, tx) ``` + + +Some notes from consensus meeting +--------------------------------- + +applyTx -- 1st phase --- applyShelleyTx + dig into applyShelleyBaseTx + applyAlonzoBasedTx + -- 2nd phase + + Core.Tx ==> has 'isValid' code for transaction + defaultApplyShelleyBasedTx (basic function from Shelley?) + +reapplyShelleyTx -- skips some checks (e.g. signatures, +if user keys didn't change; does not scripts again); we see +validity interval + * Applied same transactions to different Ledger state + * If my selection changes, I need to revalidate all + transactions. + +everything applies to Shelley. +Either Byron, or Shelley. + +Block diagram of a full block (Ouroboros consensus) +... intersect-mbo.org ... diff --git a/modules/tx_validator_phase1/src/state.rs b/modules/tx_validator_phase1/src/state.rs new file mode 100644 index 00000000..f9ec6fee --- /dev/null +++ b/modules/tx_validator_phase1/src/state.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; +use acropolis_common::BlockInfo; +use acropolis_common::messages::{ProtocolParamsMessage, RawTxsMessage}; +use acropolis_common::validation::ValidationStatus; +use crate::TxValidatorPhase1StateConfig; + +pub struct State { + pub config: Arc, + params: Option +} + + +impl State { + pub fn new(config: Arc) -> Self { + Self { config, params: None } + } + + pub async fn process_params( + &mut self, _blk: BlockInfo, prm: ProtocolParamsMessage + ) -> anyhow::Result<()> { + self.params = Some(prm); + Ok(()) + } + + pub async fn process_transactions( + &mut self, _blk: &BlockInfo, _trx: &RawTxsMessage + ) -> anyhow::Result { + Ok(ValidationStatus::Go) + } +} + diff --git a/modules/tx_validator_phase1/src/tx_validator_phase1.rs b/modules/tx_validator_phase1/src/tx_validator_phase1.rs index cb0c137d..c7bf20f6 100644 --- a/modules/tx_validator_phase1/src/tx_validator_phase1.rs +++ b/modules/tx_validator_phase1/src/tx_validator_phase1.rs @@ -1,30 +1,33 @@ //! Acropolis transaction unpacker module for Caryatid //! Unpacks transaction bodies into UTXO events -use acropolis_codec::*; +mod state; + use acropolis_common::{ messages::{ - AssetDeltasMessage, BlockTxsMessage, CardanoMessage, GovernanceProceduresMessage, Message, - TxCertificatesMessage, UTXODeltasMessage, WithdrawalsMessage, + CardanoMessage, Message, ProtocolParamsMessage, RawTxsMessage }, *, }; -use caryatid_sdk::{module, Context, Module}; -use std::{clone::Clone, fmt::Debug, sync::Arc}; +use caryatid_sdk::{module, Context, Module, Subscription}; +use std::{clone::Clone, sync::Arc}; -use anyhow::Result; +use anyhow::{anyhow, bail, Result}; use config::Config; -use pallas::codec::minicbor::encode; -use pallas::ledger::primitives::KeyValuePairs; -use pallas::ledger::{primitives, traverse, traverse::MultiEraTx}; -use tracing::{debug, error, info, info_span, Instrument}; - +use tracing::{error, info}; +use acropolis_common::validation::ValidationStatus; +use crate::state::State; //mod utxo_registry; //use crate::utxo_registry::UTxORegistry; -const DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC: &str = "cardano.txs"; -const DEFAULT_GENESIS_SUBSCRIBE_TOPIC: &str = "cardano.genesis.utxos"; +const DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC: (&str, &str) = + ("transactions-subscribe-topic", "cardano.txs"); +const DEFAULT_PROTOCOL_PARAMETERS_TOPIC: (&str, &str) = + ("parameters-topic", "cardano.protocol.parameters"); +const DEFAULT_VALIDATION_RESULT_TOPIC: (&str, &str) = + ("publish-valiadtion-result-topic", "cardano.validation.tx-phase-1"); +const DEFAULT_NETWORK_NAME: (&str, &str) = ("network-name", "mainnet"); //const CIP25_METADATA_LABEL: u64 = 721; @@ -37,513 +40,118 @@ const DEFAULT_GENESIS_SUBSCRIBE_TOPIC: &str = "cardano.genesis.utxos"; )] pub struct TxValidatorPhase1; -pub struct State { +struct TxValidatorPhase1StateConfig { + pub context: Arc>, + pub transactions_subscribe_topic: String, + pub genesis_utxos_subscribe_topic: String, + pub publish_validation_result: String, + pub params_subscribe_topic: String, + pub network_name: String, } -impl State { - pub async fn run() -> Result<()> { - info!("Validation started!"); - Ok(()) +impl TxValidatorPhase1StateConfig { + fn conf(config: &Arc, keydef: (&str, &str)) -> String { + let actual = config.get_string(keydef.0).unwrap_or(keydef.1.to_string()); + info!("Parameter value '{}' for {}", actual, keydef.0); + actual + } + + pub fn new(context: &Arc>, config: &Arc) -> Arc { + Arc::new(Self { + context: context.clone(), + transactions_subscribe_topic: Self::conf(config, DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC), + genesis_utxos_subscribe_topic: Self::conf(config, DEFAULT_PROTOCOL_PARAMETERS_TOPIC), + params_subscribe_topic: Self::conf(config, DEFAULT_PROTOCOL_PARAMETERS_TOPIC), + publish_validation_result: Self::conf(config, DEFAULT_VALIDATION_RESULT_TOPIC), + network_name: Self::conf(config, DEFAULT_NETWORK_NAME), + }) } } impl TxValidatorPhase1 { - /// Main init function - pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { - // Get configuration - let transactions_subscribe_topic = config - .get_string("subscribe-topic") - .unwrap_or(DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC.to_string()); - info!("Creating subscriber on '{transactions_subscribe_topic}'"); - - let genesis_utxos_subscribe_topic = config - .get_string("genesis-utxos-subscribe-topic") - .unwrap_or(DEFAULT_GENESIS_SUBSCRIBE_TOPIC.to_string()); - info!("Creating subscriber on '{genesis_utxos_subscribe_topic}'"); - - let publish_validation_result = config.get_string("publish-validation-result-phase1").ok(); - if let Some(ref topic) = publish_validation_result { - info!("Publishing UTXO deltas on '{topic}'"); + async fn read_parameters( + parameters_s: &mut Box>, + ) -> Result<(BlockInfo, ProtocolParamsMessage)> { + match parameters_s.read().await?.1.as_ref() { + Message::Cardano((blk, CardanoMessage::ProtocolParams(params))) => { + Ok((blk.clone(), params.clone())) + } + msg => Err(anyhow!( + "Unexpected message {msg:?} for protocol parameters topic" + )), } - let network_id: NetworkId = - config.get_string("network-id").unwrap_or("mainnet".to_string()).into(); - - // Initialize UTxORegistry - //let mut utxo_registry = UTxORegistry::default(); + } - // Subscribe to genesis and txs topics - let mut genesis_sub = context.subscribe(&genesis_utxos_subscribe_topic).await?; - let mut txs_sub = context.subscribe(&transactions_subscribe_topic).await?; + async fn read_transactions( + transaction_s: &mut Box>, + ) -> Result<(BlockInfo, RawTxsMessage)> { + match transaction_s.read().await?.1.as_ref() { + Message::Cardano((blk, CardanoMessage::ReceivedTxs(tx))) => { + Ok((blk.clone(), tx.clone())) + } + msg => Err(anyhow!( + "Unexpected message {msg:?} for transaction topic" + )), + } + } - context.clone().run(async move { - State::run().await.unwrap_or_else(|e| error!("TX validator failed: {e}")); + async fn publish_result( + config: &TxValidatorPhase1StateConfig, + block: BlockInfo, + result: ValidationStatus, + ) -> Result<()> { + let packed_message = Arc::new(Message::Cardano(( + block.clone(), + CardanoMessage::BlockValidation(result), + ))); + let context = config.context.clone(); + let topic = config.publish_validation_result.clone(); + + tokio::spawn(async move { + context + .publish(&topic, packed_message) + .await + .unwrap_or_else(|e| tracing::error!("Failed to publish: {e}")); }); Ok(()) } -} - -/* - let publish_asset_deltas_topic = config.get_string("publish-asset-deltas-topic").ok(); - if let Some(ref topic) = publish_asset_deltas_topic { - info!("Publishing native asset deltas on '{topic}'"); - } - let publish_withdrawals_topic = config.get_string("publish-withdrawals-topic").ok(); - if let Some(ref topic) = publish_withdrawals_topic { - info!("Publishing withdrawals on '{topic}'"); - } - - let publish_certificates_topic = config.get_string("publish-certificates-topic").ok(); - if let Some(ref topic) = publish_certificates_topic { - info!("Publishing certificates on '{topic}'"); - } - - let publish_governance_procedures_topic = - config.get_string("publish-governance-topic").ok(); - if let Some(ref topic) = publish_governance_procedures_topic { - info!("Publishing governance procedures on '{topic}'"); - } - - let publish_block_txs_topic = config.get_string("publish-block-txs-topic").ok(); - if let Some(ref topic) = publish_block_txs_topic { - info!("Publishing block txs on '{topic}'"); - } -*/ - -/* - fn decode_updates( - dest: &mut Vec, - proposals: &KeyValuePairs, - epoch: u64, - map: impl Fn(&EraSpecificUpdateProposals) -> Result>, - ) { - let mut update = AlonzoBabbageUpdateProposal { - proposals: Vec::new(), - enactment_epoch: epoch, - }; - - for (hash_bytes, vote) in proposals.iter() { - let hash = match GenesisKeyhash::try_from(hash_bytes.as_ref()) { - Ok(h) => h, - Err(e) => { - error!("Invalid genesis keyhash in protocol parameter update: {e}"); - continue; + async fn run(state: &mut State, + mut _gen: Box>, + mut txs: Box>, + mut params: Box> + ) -> Result<()> { + loop { + let (trx_b, trx) = Self::read_transactions(&mut txs).await?; + if trx_b.new_epoch { + let (prm_b, prm) = Self::read_parameters(&mut params).await?; + if prm_b != trx_b { + bail!("Blocks are out of sync: transaction {trx_b:?} != params {prm_b:?}"); } - }; - - match map(vote) { - Ok(upd) => update.proposals.push((hash, upd)), - Err(e) => error!("Cannot convert protocol param update {vote:?}: {e}"), + state.process_params(prm_b, prm).await?; } + let response = state.process_transactions(&trx_b, &trx).await?; + Self::publish_result(&state.config, trx_b, response).await?; } - - dest.push(update); } + /// Main init function pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { // Get configuration - let transactions_subscribe_topic = config - .get_string("subscribe-topic") - .unwrap_or(DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC.to_string()); - info!("Creating subscriber on '{transactions_subscribe_topic}'"); - - let genesis_utxos_subscribe_topic = config - .get_string("genesis-utxos-subscribe-topic") - .unwrap_or(DEFAULT_GENESIS_SUBSCRIBE_TOPIC.to_string()); - info!("Creating subscriber on '{genesis_utxos_subscribe_topic}'"); - - let publish_utxo_deltas_topic = config.get_string("publish-utxo-deltas-topic").ok(); - if let Some(ref topic) = publish_utxo_deltas_topic { - info!("Publishing UTXO deltas on '{topic}'"); - } - - let publish_asset_deltas_topic = config.get_string("publish-asset-deltas-topic").ok(); - if let Some(ref topic) = publish_asset_deltas_topic { - info!("Publishing native asset deltas on '{topic}'"); - } - - let publish_withdrawals_topic = config.get_string("publish-withdrawals-topic").ok(); - if let Some(ref topic) = publish_withdrawals_topic { - info!("Publishing withdrawals on '{topic}'"); - } - - let publish_certificates_topic = config.get_string("publish-certificates-topic").ok(); - if let Some(ref topic) = publish_certificates_topic { - info!("Publishing certificates on '{topic}'"); - } - - let publish_governance_procedures_topic = - config.get_string("publish-governance-topic").ok(); - if let Some(ref topic) = publish_governance_procedures_topic { - info!("Publishing governance procedures on '{topic}'"); - } - - let publish_block_txs_topic = config.get_string("publish-block-txs-topic").ok(); - if let Some(ref topic) = publish_block_txs_topic { - info!("Publishing block txs on '{topic}'"); - } - - let network_id: NetworkId = - config.get_string("network-id").unwrap_or("mainnet".to_string()).into(); - - // Initialize UTxORegistry - let mut utxo_registry = UTxORegistry::default(); + let config = TxValidatorPhase1StateConfig::new(&context, &config); // Subscribe to genesis and txs topics - let mut genesis_sub = context.subscribe(&genesis_utxos_subscribe_topic).await?; - let mut txs_sub = context.subscribe(&transactions_subscribe_topic).await?; + let gen_sub = context.subscribe(&config.genesis_utxos_subscribe_topic).await?; + let txs_sub = context.subscribe(&config.transactions_subscribe_topic).await?; + let params_sub = context.subscribe(&config.params_subscribe_topic).await?; context.clone().run(async move { - // Initialize TxRegistry with genesis utxos - let (_, message) = genesis_sub.read().await - .expect("failed to read genesis utxos"); - match message.as_ref() { - Message::Cardano((_block, CardanoMessage::GenesisUTxOs(genesis_msg))) => { - utxo_registry.bootstrap_from_genesis_utxos(&genesis_msg.utxos); - info!("Seeded registry with {} genesis utxos", genesis_msg.utxos.len()); - } - other => panic!("expected GenesisUTxOs, got {:?}", other), - } - loop { - let Ok((_, message)) = txs_sub.read().await else { return; }; - match message.as_ref() { - Message::Cardano((block, CardanoMessage::ReceivedTxs(txs_msg))) => { - let span = info_span!("tx_unpacker.run", block = block.number); - - async { - if tracing::enabled!(tracing::Level::DEBUG) { - debug!("Received {} txs for slot {}", - txs_msg.txs.len(), block.slot); - } - - let mut utxo_deltas = Vec::new(); - let mut asset_deltas = Vec::new(); - let mut cip25_metadata_updates = Vec::new(); - let mut withdrawals = Vec::new(); - let mut certificates = Vec::new(); - let mut voting_procedures = Vec::new(); - let mut proposal_procedures = Vec::new(); - let mut alonzo_babbage_update_proposals = Vec::new(); - let mut total_output: u128 = 0; - let mut total_fees: u64 = 0; - let total_txs = txs_msg.txs.len() as u64; - - // handle rollback or advance registry to the next block - let block_number = block.number as u32; - if block.status == BlockStatus::RolledBack { - if let Err(e) = utxo_registry.rollback_before(block_number) { - error!("rollback_before({}) failed: {}", block_number, e); - } - utxo_registry.next_block(); - } - - for (tx_index , raw_tx) in txs_msg.txs.iter().enumerate() { - let tx_index = tx_index as u16; - - // Parse the tx - match MultiEraTx::decode(raw_tx) { - Ok(tx) => { - let tx_hash: TxHash = tx.hash().to_vec().try_into().expect("invalid tx hash length"); - let tx_identifier = TxIdentifier::new(block_number, tx_index); - - let inputs = tx.consumes(); - let outputs = tx.produces(); - let certs = tx.certs(); - let tx_withdrawals = tx.withdrawals_sorted_set(); - let mut props = None; - let mut votes = None; - - if tracing::enabled!(tracing::Level::DEBUG) { - debug!("Decoded tx with {} inputs, {} outputs, {} certs", - inputs.len(), outputs.len(), certs.len()); - } - - if publish_utxo_deltas_topic.is_some() { - // Add all the inputs - for input in inputs { // MultiEraInput - // Lookup and remove UTxOIdentifier from registry - let oref = input.output_ref(); - let tx_ref = TxOutRef::new(TxHash::from(**oref.hash()), oref.index() as u16); - - match utxo_registry.consume(&tx_ref) { - Ok(tx_identifier) => { - // Add TxInput to utxo_deltas - utxo_deltas.push(UTXODelta::Input(TxInput { - utxo_identifier: UTxOIdentifier::new( - tx_identifier.block_number(), - tx_identifier.tx_index(), - tx_ref.output_index, - ), - })); - } - Err(e) => { - error!("Failed to consume input {}: {e}", tx_ref.output_index); - } - } - } - - // Add all the outputs - for (index, output) in outputs { - // Add TxOutRef to registry - match utxo_registry.add( - block_number, - tx_index, - TxOutRef { - tx_hash, - output_index: index as u16, - }, - ) { - Ok(utxo_id) => { - match output.address() { - Ok(pallas_address) => match map_parameters::map_address(&pallas_address) { - Ok(address) => { - // Add TxOutput to utxo_deltas - utxo_deltas.push(UTXODelta::Output(TxOutput { - utxo_identifier: utxo_id, - address, - value: map_parameters::map_value(&output.value()), - datum: map_parameters::map_datum(&output.datum()), - })); - - // catch all output lovelaces - total_output += output.value().coin() as u128; - } - Err(e) => error!("Output {index} in tx ignored: {e}"), - }, - Err(e) => error!("Can't parse output {index} in tx: {e}"), - } - } - Err(e) => { - error!("Failed to insert output into registry: {e}"); - } - } - } - } - - if publish_asset_deltas_topic.is_some() { - let mut tx_deltas: Vec<(PolicyId, Vec)> = Vec::new(); - - // Mint deltas - for policy_group in tx.mints().iter() { - if let Some((policy_id, deltas)) = map_parameters::map_mint_burn(policy_group) { - tx_deltas.push((policy_id, deltas)); - } - } - - if let Some(metadata) = tx.metadata().find(CIP25_METADATA_LABEL) { - let mut metadata_raw = Vec::new(); - match encode(metadata, &mut metadata_raw) { - Ok(()) => { - cip25_metadata_updates.push(metadata_raw); - } - Err(e) => { - error!("failed to encode CIP-25 metadatum: {e:#}"); - } - } - } - - if !tx_deltas.is_empty() { - asset_deltas.push((tx_identifier, tx_deltas)); - } - } - - if publish_certificates_topic.is_some() { - for ( cert_index, cert) in certs.iter().enumerate() { - match map_parameters::map_certificate(cert, tx_identifier, cert_index, network_id.clone()) { - Ok(tx_cert) => { - certificates.push(tx_cert); - }, - Err(_e) => { - // TODO error unexpected - //error!("{e}"); - } - } - } - } - - if publish_withdrawals_topic.is_some() { - for (key, value) in tx_withdrawals { - match StakeAddress::from_binary(key) { - Ok(stake_address) => { - withdrawals.push(Withdrawal { - address: stake_address, - value, - tx_identifier - }); - } - Err(e) => error!("Bad stake address: {e:#}"), - } - } - } - - if publish_governance_procedures_topic.is_some() { - //Self::decode_legacy_updates(&mut legacy_update_proposals, &block, &raw_tx); - if block.era >= Era::Shelley && block.era < Era::Babbage { - if let Ok(alonzo) = MultiEraTx::decode_for_era(traverse::Era::Alonzo, raw_tx) { - if let Some(update) = alonzo.update() { - if let Some(alonzo_update) = update.as_alonzo() { - Self::decode_updates( - &mut alonzo_babbage_update_proposals, - &alonzo_update.proposed_protocol_parameter_updates, - alonzo_update.epoch, - map_parameters::map_alonzo_protocol_param_update - ); - } - } - } - } - else if block.era >= Era::Babbage && block.era < Era::Conway{ - if let Ok(babbage) = MultiEraTx::decode_for_era(traverse::Era::Babbage, raw_tx) { - if let Some(update) = babbage.update() { - if let Some(babbage_update) = update.as_babbage() { - Self::decode_updates( - &mut alonzo_babbage_update_proposals, - &babbage_update.proposed_protocol_parameter_updates, - babbage_update.epoch, - map_parameters::map_babbage_protocol_param_update - ); - } - } - } - } - } - - if let Some(conway) = tx.as_conway() { - if let Some(ref v) = conway.transaction_body.voting_procedures { - votes = Some(v); - } - - if let Some(ref p) = conway.transaction_body.proposal_procedures { - props = Some(p); - } - } - - - if publish_governance_procedures_topic.is_some() { - if let Some(pp) = props { - // Nonempty set -- governance_message.proposal_procedures will not be empty - let mut proc_id = GovActionId { transaction_id: tx_hash, action_index: 0 }; - for (action_index, pallas_governance_proposals) in pp.iter().enumerate() { - match proc_id.set_action_index(action_index) - .and_then (|proc_id| map_parameters::map_governance_proposals_procedures(proc_id, pallas_governance_proposals)) - { - Ok(g) => proposal_procedures.push(g), - Err(e) => error!("Cannot decode governance proposal procedure {} idx {} in slot {}: {e}", proc_id, action_index, block.slot) - } - } - } - - if let Some(pallas_vp) = votes { - // Nonempty set -- governance_message.voting_procedures will not be empty - match map_parameters::map_all_governance_voting_procedures(pallas_vp) { - Ok(vp) => voting_procedures.push((tx_hash, vp)), - Err(e) => error!("Cannot decode governance voting procedures in slot {}: {e}", block.slot) - } - } - } - - // Capture the fees - if let Some(fee) = tx.fee() { - total_fees += fee; - } - }, - - Err(e) => error!("Can't decode transaction in slot {}: {e}", - block.slot) - } - } - - utxo_registry.next_block(); - - // Publish messages in parallel - let mut futures = Vec::new(); - if let Some(ref topic) = publish_utxo_deltas_topic { - let msg = Message::Cardano(( - block.clone(), - CardanoMessage::UTXODeltas(UTXODeltasMessage { - deltas: utxo_deltas, - }) - )); - - futures.push(context.message_bus.publish(topic, Arc::new(msg))); - } - - if let Some(ref topic) = publish_asset_deltas_topic { - let msg = Message::Cardano(( - block.clone(), - CardanoMessage::AssetDeltas(AssetDeltasMessage { - deltas: asset_deltas, - cip25_metadata_updates - }) - )); - - futures.push(context.message_bus.publish(topic, Arc::new(msg))); - } - - if let Some(ref topic) = publish_withdrawals_topic { - let msg = Message::Cardano(( - block.clone(), - CardanoMessage::Withdrawals(WithdrawalsMessage { - withdrawals, - }) - )); - - futures.push(context.message_bus.publish(topic, Arc::new(msg))); - } - - if let Some(ref topic) = publish_certificates_topic { - let msg = Message::Cardano(( - block.clone(), - CardanoMessage::TxCertificates(TxCertificatesMessage { - certificates, - }) - )); - - futures.push(context.message_bus.publish(topic, Arc::new(msg))); - } - - if let Some(ref topic) = publish_governance_procedures_topic { - let governance_msg = Arc::new(Message::Cardano(( - block.clone(), - CardanoMessage::GovernanceProcedures( - GovernanceProceduresMessage { - voting_procedures, - proposal_procedures, - alonzo_babbage_updates: alonzo_babbage_update_proposals - }) - ))); - - futures.push(context.message_bus.publish(topic, - governance_msg.clone())); - } - - if let Some(ref topic) = publish_block_txs_topic { - let msg = Message::Cardano(( - block.clone(), - CardanoMessage::BlockInfoMessage(BlockTxsMessage { - total_txs, - total_output, - total_fees - }) - )); - - futures.push(context.message_bus.publish(topic, Arc::new(msg))); - } - - join_all(futures) - .await - .into_iter() - .filter_map(Result::err) - .for_each(|e| error!("Failed to publish: {e}")); - }.instrument(span).await; - } - - _ => error!("Unexpected message type: {message:?}") - } - } + let mut state = State::new(config.clone()); + TxValidatorPhase1::run(&mut state, gen_sub, txs_sub, params_sub) + .await.unwrap_or_else(|e| error!("TX validator failed: {e}")); }); Ok(()) } -*/ +} From c572a5e93a7d7a34ce4d1dea4844783b27aef86c Mon Sep 17 00:00:00 2001 From: Dmitry Shtukenberg Date: Fri, 7 Nov 2025 17:47:00 +0300 Subject: [PATCH 03/18] Warning removed --- modules/tx_validator_phase1/src/tx_validator_phase1.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/tx_validator_phase1/src/tx_validator_phase1.rs b/modules/tx_validator_phase1/src/tx_validator_phase1.rs index c7bf20f6..9883e7e0 100644 --- a/modules/tx_validator_phase1/src/tx_validator_phase1.rs +++ b/modules/tx_validator_phase1/src/tx_validator_phase1.rs @@ -46,6 +46,7 @@ struct TxValidatorPhase1StateConfig { pub genesis_utxos_subscribe_topic: String, pub publish_validation_result: String, pub params_subscribe_topic: String, + #[allow(dead_code)] pub network_name: String, } From 8d752f9d261bbb8d9881d78d0ae70e69892b119c Mon Sep 17 00:00:00 2001 From: Dmitry Shtukenberg Date: Mon, 10 Nov 2025 15:06:29 +0300 Subject: [PATCH 04/18] CBOR Validation error added --- common/src/validation.rs | 3 ++ modules/tx_validator_phase1/src/state.rs | 46 +++++++++++++++++-- .../src/tx_validator_phase1.rs | 6 ++- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/common/src/validation.rs b/common/src/validation.rs index 1501af67..5f649a50 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -14,6 +14,9 @@ pub enum ValidationError { #[error("KES failure")] BadKES, + #[error("CBOR Decoding error")] + CborDecodeError(usize, String), + #[error("Doubly spent UTXO: {0}")] DoubleSpendUTXO(String), } diff --git a/modules/tx_validator_phase1/src/state.rs b/modules/tx_validator_phase1/src/state.rs index f9ec6fee..5eeb2ef7 100644 --- a/modules/tx_validator_phase1/src/state.rs +++ b/modules/tx_validator_phase1/src/state.rs @@ -1,14 +1,21 @@ +use std::borrow::Cow; use std::sync::Arc; +use pallas::ledger::primitives::{alonzo, byron}; +use pallas::ledger::traverse::MultiEraTx; use acropolis_common::BlockInfo; use acropolis_common::messages::{ProtocolParamsMessage, RawTxsMessage}; -use acropolis_common::validation::ValidationStatus; +use acropolis_common::validation::{ValidationError, ValidationStatus}; use crate::TxValidatorPhase1StateConfig; +use anyhow::Result; pub struct State { pub config: Arc, params: Option } +fn convert_from_pallas_alonzo<'b> (tx: &alonzo::MintedTx<'b>) { + return Transaction{tx} +} impl State { pub fn new(config: Arc) -> Self { @@ -17,14 +24,43 @@ impl State { pub async fn process_params( &mut self, _blk: BlockInfo, prm: ProtocolParamsMessage - ) -> anyhow::Result<()> { + ) -> Result<()> { self.params = Some(prm); Ok(()) } - pub async fn process_transactions( - &mut self, _blk: &BlockInfo, _trx: &RawTxsMessage - ) -> anyhow::Result { + /// Byron validation is not acutally performed, so it's always returns 'Go' + fn validate_byron<'b>(&self, _tx: Box>>) + -> Result + { + Ok(ValidationStatus::Go) + } + + pub fn process_transactions( + &mut self, _blk: &BlockInfo, txs_msg: &RawTxsMessage + ) -> Result { + for (tx_index , raw_tx) in txs_msg.txs.iter().enumerate() { + // Parse the tx + let res = match MultiEraTx::decode(raw_tx) { + Err(e) => + ValidationStatus::NoGo( + ValidationError::CborDecodeError(tx_index, e.to_string()) + ), + Ok(MultiEraTx::Byron(byron_tx)) => self.validate_byron(byron_tx)?, + + Ok(MultiEraTx::AlonzoCompatible(tx, _)) => + self.validate_tx(convert_from_pallas_alonzo(tx))?, + Ok(MultiEraTx::Babbage(tx)) => + self.validate_tx(convert_from_pallas_babbage(tx))?, + Ok(MultiEraTx::Conway(tx)) => + self.validate_tx(convert_from_conway_babbage(tx))?, + _ => ValidationStatus::NoGo(ValidationError::CborDecodeError(0, "".to_string())) + }; + + if let ValidationStatus::NoGo(_) = &res { + return Ok(res); + } + } Ok(ValidationStatus::Go) } } diff --git a/modules/tx_validator_phase1/src/tx_validator_phase1.rs b/modules/tx_validator_phase1/src/tx_validator_phase1.rs index 9883e7e0..5e510819 100644 --- a/modules/tx_validator_phase1/src/tx_validator_phase1.rs +++ b/modules/tx_validator_phase1/src/tx_validator_phase1.rs @@ -101,6 +101,10 @@ impl TxValidatorPhase1 { block: BlockInfo, result: ValidationStatus, ) -> Result<()> { + if let ValidationStatus::NoGo(res) = &result { + error!("Cannot validate transaction: {:?}", res); + } + let packed_message = Arc::new(Message::Cardano(( block.clone(), CardanoMessage::BlockValidation(result), @@ -132,7 +136,7 @@ impl TxValidatorPhase1 { } state.process_params(prm_b, prm).await?; } - let response = state.process_transactions(&trx_b, &trx).await?; + let response = state.process_transactions(&trx_b, &trx)?; Self::publish_result(&state.config, trx_b, response).await?; } } From 4eff40e96f245c3060fad8c7035eb9eff0b6c8ba Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 21 Nov 2025 16:53:52 +0100 Subject: [PATCH 05/18] feat: add transaction validation error enums for shelley era and added test cases for validate ttl function with invalid transaction cbor --- codec/src/map_parameters.rs | 87 ++++++++------- common/src/types.rs | 61 ++++++++++- common/src/validation.rs | 100 +++++++++++++++++- modules/tx_unpacker/src/tx_unpacker.rs | 7 +- modules/tx_unpacker/src/validations/mod.rs | 1 + .../tx_unpacker/src/validations/shelley.rs | 80 ++++++++++++++ .../invalid-ttl/tx.cbor | 1 + .../tx.cbor | 1 + modules/tx_validator_phase1/src/state.rs | 98 ++++++++++------- .../src/tx_validator_phase1.rs | 32 +++--- processes/omnibus/src/main.rs | 2 +- 11 files changed, 368 insertions(+), 102 deletions(-) create mode 100644 modules/tx_unpacker/src/validations/mod.rs create mode 100644 modules/tx_unpacker/src/validations/shelley.rs create mode 100644 modules/tx_unpacker/src/validations/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/tx.cbor create mode 100644 modules/tx_unpacker/src/validations/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor diff --git a/codec/src/map_parameters.rs b/codec/src/map_parameters.rs index 0d1d49d1..acf8f5b6 100644 --- a/codec/src/map_parameters.rs +++ b/codec/src/map_parameters.rs @@ -12,18 +12,18 @@ use pallas::ledger::{ *, }; +use crate::map_parameters; use acropolis_common::hash::Hash; +use acropolis_common::validation::ValidationError; use acropolis_common::{ protocol_params::{Nonce, NonceVariant, ProtocolVersion}, rational_number::RationalNumber, *, }; use pallas_primitives::conway::PseudoScript; -use std::collections::{HashMap, HashSet}; use pallas_traverse::{MultiEraInput, MultiEraOutput, MultiEraTx}; +use std::collections::{HashMap, HashSet}; use tracing::error; -use acropolis_common::validation::ValidationError; -use crate::map_parameters; /// Map Pallas Network to our NetworkId pub fn map_network(network: addresses::Network) -> Result { @@ -977,35 +977,46 @@ pub fn map_all_governance_voting_procedures( Ok(procs) } -pub struct TransactionRefInfo { - -} +pub struct TransactionRefInfo {} -pub fn map_transaction_refs(inputs: &Vec, outputs: &Vec<(usize, MultiEraOutput)>) - -> (Vec, Vec) -{ +pub fn map_transaction_refs( + inputs: &Vec, + outputs: &Vec<(usize, MultiEraOutput)>, +) -> (Vec, Vec) { let mut ref_inps = Vec::new(); - for input in inputs { // MultiEraInput + for input in inputs { + // MultiEraInput let oref = input.output_ref(); let tx_ref = TxOutRef::new(TxHash::from(**oref.hash()), oref.index() as u16); ref_inps.push(tx_ref); - }; + } let mut ref_outs = Vec::new(); - for (index, output) in outputs { - - } + for (index, output) in outputs {} (ref_inps, ref_outs) } -pub fn map_one_transaction(block_number: u32, tx_index: u16, tx: &MultiEraTx) -> - (Vec, Vec<(TxOutRef, TxOutput)>, u128, Vec) -{ +pub fn map_one_transaction( + block_number: u32, + tx_index: u16, + tx: &MultiEraTx, +) -> ( + Vec, + Vec<(TxOutRef, TxOutput)>, + u128, + Vec, +) { let Ok(tx_hash) = tx.hash().to_vec().try_into() else { - return (vec![], vec![], 0, vec![ValidationError::MalformedTransaction( - tx_index, format!("Tx has incorrect hash length ({:?})", tx.hash().to_vec()) - )]) + return ( + vec![], + vec![], + 0, + vec![ValidationError::MalformedTransaction( + tx_index, + format!("Tx has incorrect hash length ({:?})", tx.hash().to_vec()), + )], + ); }; let inputs = tx.consumes(); @@ -1016,7 +1027,8 @@ pub fn map_one_transaction(block_number: u32, tx_index: u16, tx: &MultiEraTx) -> let mut total_output = 0; let mut errors = Vec::new(); - for input in inputs { // MultiEraInput + for input in inputs { + // MultiEraInput let oref = input.output_ref(); let tx_ref = TxOutRef::new(TxHash::from(**oref.hash()), oref.index() as u16); @@ -1030,36 +1042,37 @@ pub fn map_one_transaction(block_number: u32, tx_index: u16, tx: &MultiEraTx) -> output_index: index as u16, }; - let utxo_id = UTxOIdentifier::new( - block_number, - tx_index, - tx_out_ref.output_index, - ); + let utxo_id = UTxOIdentifier::new(block_number, tx_index, tx_out_ref.output_index); match output.address() { Ok(pallas_address) => match map_address(&pallas_address) { Ok(address) => { // Add TxOutput to utxo_deltas - tx_outputs.push((tx_out_ref, TxOutput { - utxo_identifier: utxo_id, - address, - value: map_value(&output.value()), - datum: map_datum(&output.datum()), - reference_script: map_reference_script(&output.script_ref()) - })); + tx_outputs.push(( + tx_out_ref, + TxOutput { + utxo_identifier: utxo_id, + address, + value: map_value(&output.value()), + datum: map_datum(&output.datum()), + reference_script: map_reference_script(&output.script_ref()), + }, + )); // catch all output lovelaces total_output += output.value().coin() as u128; } Err(e) => { errors.push(ValidationError::MalformedTransaction( - tx_index, format!("Output {index} in tx ignored: {e}") + tx_index, + format!("Output {index} in tx ignored: {e}"), )); - }, + } }, Err(e) => errors.push(ValidationError::MalformedTransaction( - tx_index, format!("Can't parse output {index} in tx: {e}")) - ) + tx_index, + format!("Can't parse output {index} in tx: {e}"), + )), } } diff --git a/common/src/types.rs b/common/src/types.rs index d7c90545..7708f1db 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -60,6 +60,19 @@ impl From for NetworkId { } } +impl Display for NetworkId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + NetworkId::Mainnet => "mainnet", + NetworkId::Testnet => "testnet", + } + ) + } +} + /// Protocol era #[derive( Debug, @@ -322,7 +335,14 @@ impl AssetName { } #[derive( - Debug, Clone, serde::Serialize, serde::Deserialize, minicbor::Encode, minicbor::Decode, + Debug, + Clone, + serde::Serialize, + serde::Deserialize, + minicbor::Encode, + minicbor::Decode, + PartialEq, + Eq, )] pub struct NativeAsset { #[n(0)] @@ -342,7 +362,7 @@ pub struct NativeAssetDelta { } /// Datum (inline or hash) -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub enum Datum { Hash(Vec), Inline(Vec), @@ -358,7 +378,7 @@ pub enum ReferenceScript { } /// Value (lovelace + multiasset) -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, Eq)] pub struct Value { pub lovelace: u64, pub assets: NativeAssets, @@ -561,7 +581,7 @@ pub struct UTXOValue { } /// Transaction output (UTXO) -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] pub struct TxOutput { /// Identifier for this UTxO pub utxo_identifier: UTxOIdentifier, @@ -704,6 +724,39 @@ impl fmt::Display for UTxOIdentifier { } } +#[derive( + Debug, + Clone, + serde::Serialize, + serde::Deserialize, + minicbor::Encode, + minicbor::Decode, + PartialEq, + Eq, +)] +pub struct UTxOIdentifierSet(#[n(0)] pub HashSet); + +impl fmt::Display for UTxOIdentifierSet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let items: Vec = self.0.iter().map(|id| id.to_string()).collect(); + write!(f, "[{}]", items.join(", ")) + } +} + +impl From> for UTxOIdentifierSet { + fn from(set: HashSet) -> Self { + UTxOIdentifierSet(set) + } +} + +impl std::ops::Deref for UTxOIdentifierSet { + type Target = HashSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + // Full TxOutRef stored in UTxORegistry for UTxOIdentifier lookups #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct TxOutRef { diff --git a/common/src/validation.rs b/common/src/validation.rs index 54fbe671..809d8875 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -7,7 +7,99 @@ use std::array::TryFromSliceError; use thiserror::Error; -use crate::{protocol_params::Nonce, GenesisKeyhash, PoolId, Slot, TxHash, VrfKeyHash}; +use crate::{ + protocol_params::Nonce, Address, Era, GenesisKeyhash, Lovelace, NetworkId, PoolId, Slot, + StakeAddress, TxOutput, UTxOIdentifierSet, Value, VrfKeyHash, +}; + +/// Transaction Validation Error +/// +/// Shelley Era Errors: +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L343 +/// +/// Allegra Era Errors: +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/allegra/impl/src/Cardano/Ledger/Allegra/Rules/Utxo.hs#L160 +/// +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Error, PartialEq, Eq)] +pub enum TransactionValidationError { + /// **Cause**: Raw Transaction CBOR is invalid + #[error("CBOR Decoding error: {0}")] + CborDecodeError(String), + + /// **Cause**: Transaction is not in correct form. + /// e.g. some field is missing from transaction body, when it is required. + /// Reference: + /// Shelley: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/cddl-files/shelley.cddl + /// Allegra: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/allegra/impl/cddl-files/allegra.cddl + /// Alonzo: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/alonzo/impl/cddl-files/alonzo.cddl + #[error("Malformed Transaction: era={era}, reason={reason}")] + MalformedTransaction { era: Era, reason: String }, + + /// ------------ Shelley Era Errors ------------ + /// **Cause:** The UTXO has expired + #[error("Expired UTXO: ttl={ttl}, current_slot={current_slot}")] + ExpiredUTxO { ttl: Slot, current_slot: Slot }, + + /// **Cause:** The input set is empty. (genesis transactions are exceptions) + #[error("Input Set Empty UTXO")] + InputSetEmptyUTxO, + + /// **Cause:** The fee is too small. + #[error("Fee is too small: supplied={supplied}, required={required}")] + FeeTooSmallUTxO { + supplied: Lovelace, + required: Lovelace, + }, + + /// **Cause:** Some of transaction inputs are not in current UTxOs set. + #[error("Bad inputs: bad_inputs={bad_inputs}")] + BadInputsUTxO { bad_inputs: UTxOIdentifierSet }, + + /// **Cause:** Some of transaction outputs are on a different network than the expected one. + #[error( + "Wrong network: expected={expected}, wrong_addresses=[{}]", + wrong_addresses + .iter() + .map(|a| a.to_string().unwrap_or_else(|_| "invalid address".to_string())) + .collect::>() + .join(", ") + )] + WrongNetwork { + expected: NetworkId, + wrong_addresses: Vec
, + }, + + /// **Cause:** Some of withdrawal accounts are on a different network than the expected one. + #[error( + "Wrong network withdrawal: expected={expected}, wrong_accounts=[{}]", + wrong_accounts + .iter() + .map(|a| a.to_string().unwrap_or_else(|_| "invalid address".to_string())) + .collect::>() + .join(", ") + )] + WrongNetworkWithdrawal { + expected: NetworkId, + wrong_accounts: Vec, + }, + + /// **Cause:** The value of the UTXO is not conserved. + /// Consumed = inputs + withdrawals + refunds, Produced = outputs + fees + deposits + #[error("Value not conserved: consumed={consumed:?}, produced={produced:?}]")] + ValueNotConservedUTxO { consumed: Value, produced: Value }, + + /// **Cause:** Some of the outputs don't have minimum required lovelace + #[error("Output too small UTxO: small_outputs={small_outputs:?}")] + OutputTooSmallUTxO { small_outputs: Vec }, + + /// **Cause:** Some of the outputs have boot address (only byron-era) attributes that are too big + #[error("Output boot address attrs too big: large_outputs={large_outputs:?}")] + OutputBootAddrAttrsTooBig { large_outputs: Vec }, + + /// **Cause:** The transaction size is too big. + #[error("Max tx size: supplied={supplied}, max={max}")] + MaxTxSizeUTxO { supplied: u32, max: u32 }, +} /// Validation error #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Error)] @@ -18,6 +110,12 @@ pub enum ValidationError { #[error("KES failure: {0}")] BadKES(#[from] KesValidationError), + #[error("Invalid Transaction: tx-index={tx_index}, error={error}")] + BadTransaction { + tx_index: u16, + error: TransactionValidationError, + }, + #[error("CBOR Decoding error")] CborDecodeError(usize, String), diff --git a/modules/tx_unpacker/src/tx_unpacker.rs b/modules/tx_unpacker/src/tx_unpacker.rs index ea393c18..0f697a8b 100644 --- a/modules/tx_unpacker/src/tx_unpacker.rs +++ b/modules/tx_unpacker/src/tx_unpacker.rs @@ -10,7 +10,7 @@ use acropolis_common::{ *, }; use caryatid_sdk::{module, Context, Module}; -use std::{clone::Clone, fmt::Debug, sync::Arc}; +use std::{clone::Clone, fmt::Debug, str::FromStr, sync::Arc}; use anyhow::Result; use config::Config; @@ -19,8 +19,8 @@ use pallas::codec::minicbor::encode; use pallas::ledger::primitives::KeyValuePairs; use pallas::ledger::{primitives, traverse, traverse::MultiEraTx}; use tracing::{debug, error, info, info_span, Instrument}; - mod utxo_registry; +mod validations; use crate::utxo_registry::UTxORegistry; const DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC: &str = "cardano.txs"; @@ -171,6 +171,9 @@ impl TxUnpacker { match MultiEraTx::decode(raw_tx) { Ok(tx) => { let tx_hash: TxHash = tx.hash().to_vec().try_into().expect("invalid tx hash length"); + if tx_hash == TxHash::from_str("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e").unwrap() { + println!("Tx Cbor: {:?}", hex::encode(raw_tx)); + } let tx_identifier = TxIdentifier::new(block_number, tx_index); let inputs = tx.consumes(); diff --git a/modules/tx_unpacker/src/validations/mod.rs b/modules/tx_unpacker/src/validations/mod.rs new file mode 100644 index 00000000..ea3e3b8b --- /dev/null +++ b/modules/tx_unpacker/src/validations/mod.rs @@ -0,0 +1 @@ +pub mod shelley; diff --git a/modules/tx_unpacker/src/validations/shelley.rs b/modules/tx_unpacker/src/validations/shelley.rs new file mode 100644 index 00000000..29e1bcd1 --- /dev/null +++ b/modules/tx_unpacker/src/validations/shelley.rs @@ -0,0 +1,80 @@ +//! Shelley era transaction validation +//! Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L343 + +use acropolis_common::{validation::TransactionValidationError, Era}; +use pallas::ledger::{ + primitives::alonzo, + traverse::{Era as PallasEra, MultiEraTx}, +}; + +pub fn validate_shelley_tx( + tx: &MultiEraTx, + current_slot: u64, +) -> Result<(), TransactionValidationError> { + let tx = match tx { + MultiEraTx::AlonzoCompatible(tx, PallasEra::Shelley) => tx, + _ => { + return Err(TransactionValidationError::MalformedTransaction { + era: Era::Shelley, + reason: "Transaction is not Shelley compatible".to_string(), + }) + } + }; + + validate_time_to_live(tx, current_slot)?; + Ok(()) +} + +/// Validate transaction's TTL field +/// pass if ttl >= current_slot +/// Reference +/// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L421 +pub fn validate_time_to_live( + tx: &alonzo::MintedTx, + current_slot: u64, +) -> Result<(), TransactionValidationError> { + if let Some(ttl) = tx.transaction_body.ttl { + if ttl >= current_slot { + Ok(()) + } else { + Err(TransactionValidationError::ExpiredUTxO { ttl, current_slot }) + } + } else { + Err(TransactionValidationError::MalformedTransaction { + era: Era::Shelley, + reason: "TTL is missing".to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pallas::{codec, ledger::primitives::alonzo::MintedTx as AlonzoMintedTx}; + + #[test] + fn test_ttl() { + let cbor_bytes = hex::decode(include_str!("tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor")).unwrap(); + let mtx = codec::minicbor::decode::(&cbor_bytes).unwrap(); + let metx = MultiEraTx::from_alonzo_compatible(&mtx, PallasEra::Shelley); + + let result = validate_shelley_tx(&metx, 7084748); + assert!(result.is_ok()); + } + + #[test] + fn test_invalid_ttl() { + let cbor_bytes = hex::decode(include_str!("tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/tx.cbor")).unwrap(); + let mtx = codec::minicbor::decode::(&cbor_bytes).unwrap(); + let metx = MultiEraTx::from_alonzo_compatible(&mtx, PallasEra::Shelley); + + let result = validate_shelley_tx(&metx, 7084748); + assert_eq!( + result.err().unwrap(), + TransactionValidationError::ExpiredUTxO { + ttl: 7084747, + current_slot: 7084748 + } + ); + } +} diff --git a/modules/tx_unpacker/src/validations/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/tx.cbor b/modules/tx_unpacker/src/validations/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/tx.cbor new file mode 100644 index 00000000..34520c9e --- /dev/null +++ b/modules/tx_unpacker/src/validations/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/tx.cbor @@ -0,0 +1 @@ +84A40081825820278EC9A3DFB551288AFFD8D84B2D6C70B56A153AD1FD43E7B18A5528F687733E01018282584C82D818584283581CE8BD78295559F7EDD927190EC36F7FBE438F74CF0C9380C717AEB56FA101581E581C2B0B011BA3683D490846FD2A91BF44F73A59B1F93E5E753FC1AEBED1001A6363D6C21B000000BF32F3680F825839019902792B16C60946E986821C08CAD447C0C2292440F13F709A5CACFA1AD46761EE3214603CBFBC0CEBB6EDA23ADBDB36E8087A2A1C16765E1B0000000BA4324C40021A0002A1FD031A006C1ACBA1028184582041A0B7A66D454ED0465E7B601D1FF1FE3CDDCDF5D127D1DA108CBB3555747CD158404C832202E28859CEFD3169EF238963A887A396E7773519B68721F9C059ABF7BEC56422111F4C66B13A116C2D10329A289C815FCE6D03F6BE903E74C3959BC703582066810329C45D09FD3E396A4EB1FF5E9D4632D5E156B5B320AD992C34D67E25F75822A101581E581C2B0B011BA3683D07B5F91D2AB76A8CAAB0E4DDA6099218E20432E4CCF5F6 \ No newline at end of file diff --git a/modules/tx_unpacker/src/validations/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor b/modules/tx_unpacker/src/validations/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor new file mode 100644 index 00000000..11eddb3a --- /dev/null +++ b/modules/tx_unpacker/src/validations/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor @@ -0,0 +1 @@ +84a40081825820278ec9a3dfb551288affd8d84b2d6c70b56a153ad1fd43e7b18a5528f687733e01018282584c82d818584283581ce8bd78295559f7edd927190ec36f7fbe438f74cf0c9380c717aeb56fa101581e581c2b0b011ba3683d490846fd2a91bf44f73a59b1f93e5e753fc1aebed1001a6363d6c21b000000bf32f3680f825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40021a0002a1fd031a006c36c5a1028184582041a0b7a66d454ed0465e7b601d1ff1fe3cddcdf5d127d1da108cbb3555747cd158404c832202e28859cefd3169ef238963a887a396e7773519b68721f9c059abf7bec56422111f4c66b13a116c2d10329a289c815fce6d03f6be903e74c3959bc703582066810329c45d09fd3e396a4eb1ff5e9d4632d5e156b5b320ad992c34d67e25f75822a101581e581c2b0b011ba3683d07b5f91d2ab76a8caab0e4dda6099218e20432e4ccf5f6 \ No newline at end of file diff --git a/modules/tx_validator_phase1/src/state.rs b/modules/tx_validator_phase1/src/state.rs index 378a0152..1cd58943 100644 --- a/modules/tx_validator_phase1/src/state.rs +++ b/modules/tx_validator_phase1/src/state.rs @@ -1,15 +1,18 @@ -use std::borrow::Cow; -use std::collections::HashMap; -use std::sync::Arc; -use pallas::ledger::primitives::{alonzo, byron}; -use pallas::ledger::traverse::{MultiEraPolicyAssets, MultiEraTx, MultiEraValue}; -use acropolis_common::{AssetName, BlockInfo, NativeAsset, NativeAssets, TxHash, TxIdentifier, TxOutRef, TxOutput, UTxOIdentifier, Value}; +use crate::TxValidatorPhase1StateConfig; +use acropolis_codec::map_parameters; use acropolis_common::messages::{ProtocolParamsMessage, RawTxsMessage}; use acropolis_common::validation::{ValidationError, ValidationStatus}; -use crate::TxValidatorPhase1StateConfig; +use acropolis_common::{ + AssetName, BlockInfo, NativeAsset, NativeAssets, TxHash, TxIdentifier, TxOutRef, TxOutput, + UTxOIdentifier, Value, +}; use anyhow::Result; +use pallas::ledger::primitives::{alonzo, byron}; +use pallas::ledger::traverse::{MultiEraPolicyAssets, MultiEraTx, MultiEraValue}; +use std::borrow::Cow; +use std::collections::HashMap; +use std::sync::Arc; use tracing::error; -use acropolis_codec::map_parameters; // TODO: make something with separate utxo registres #[derive(Clone, Default)] @@ -92,63 +95,76 @@ pub fn map_value(pallas_value: &MultiEraValue) -> Value { struct Transaction { inputs: Vec, - outputs: Vec + outputs: Vec, } impl State { pub fn new(config: Arc) -> Self { - Self { config, params: None, utxos_registry: UTxORegistry::default() } + Self { + config, + params: None, + utxos_registry: UTxORegistry::default(), + } } pub async fn process_params( - &mut self, _blk: BlockInfo, prm: ProtocolParamsMessage + &mut self, + _blk: BlockInfo, + prm: ProtocolParamsMessage, ) -> Result<()> { self.params = Some(prm); Ok(()) } /// Byron validation is not acutally performed, so it's always returns 'Go' - fn validate_byron<'b>(&self, _tx: Box>>) - -> Result - { + fn validate_byron<'b>( + &self, + _tx: Box>>, + ) -> Result { Ok(ValidationStatus::Go) } - fn convert_from_pallas_tx<'b>(&self, block_info: &BlockInfo, tx_index: u16, tx: &MultiEraTx) - -> Result> - { + fn convert_from_pallas_tx<'b>( + &self, + block_info: &BlockInfo, + tx_index: u16, + tx: &MultiEraTx, + ) -> Result> { let _certs = tx.certs(); let _tx_withdrawals = tx.withdrawals_sorted_set(); - let (tx_in_ref,tx_out,_total,err) = + let (tx_in_ref, tx_out, _total, err) = map_parameters::map_one_transaction(block_info.number as u32, tx_index, tx); if let Some(first_err) = err.into_iter().next() { - return Ok(ConversionResult::Error(first_err)) + return Ok(ConversionResult::Error(first_err)); } let mut converted_inputs = Vec::new(); let mut converted_outputs = Vec::new(); - for tx_ref in tx_in_ref { // MultiEraInput + for tx_ref in tx_in_ref { + // MultiEraInput // Lookup and remove UTxOIdentifier from registry match self.utxos_registry.live_map.get(&tx_ref) { Some(tx_identifier) => { // Add TxInput to utxo_deltas converted_inputs.push(UTxOIdentifier::new( - tx_identifier.block_number(), - tx_identifier.tx_index(), - tx_ref.output_index, - ) - ); + tx_identifier.block_number(), + tx_identifier.tx_index(), + tx_ref.output_index, + )); } None => { - return Ok(ConversionResult::Error(ValidationError::MalformedTransaction( - tx_index, - format!("Tx not found, tx {}, output index {}", - tx_ref.tx_hash, tx_ref.output_index - ) - ))); + return Ok(ConversionResult::Error( + ValidationError::MalformedTransaction( + tx_index, + format!( + "Tx not found, tx {}, output index {}", + tx_ref.tx_hash, tx_ref.output_index + ), + ), + )); } } } @@ -172,25 +188,26 @@ impl State { } pub fn process_transactions( - &mut self, blk: &BlockInfo, txs_msg: &RawTxsMessage + &mut self, + blk: &BlockInfo, + txs_msg: &RawTxsMessage, ) -> Result { - for (tx_index , raw_tx) in txs_msg.txs.iter().enumerate() { + for (tx_index, raw_tx) in txs_msg.txs.iter().enumerate() { // Parse the tx let res = match MultiEraTx::decode(raw_tx) { - Err(e) => - ValidationStatus::NoGo( - ValidationError::CborDecodeError(tx_index, e.to_string()) - ), + Err(e) => ValidationStatus::NoGo(ValidationError::CborDecodeError( + tx_index, + e.to_string(), + )), Ok(MultiEraTx::Byron(byron_tx)) => self.validate_byron(byron_tx)?, Ok(tx) => { let tx = match self.convert_from_pallas_tx(blk, tx_index as u16, &tx)? { ConversionResult::Ok(res) => res, - ConversionResult::Error(err) => - return Ok(ValidationStatus::NoGo(err)) + ConversionResult::Error(err) => return Ok(ValidationStatus::NoGo(err)), }; self.validate_tx(&tx)? - }, + } }; if let ValidationStatus::NoGo(_) = &res { @@ -200,4 +217,3 @@ impl State { Ok(ValidationStatus::Go) } } - diff --git a/modules/tx_validator_phase1/src/tx_validator_phase1.rs b/modules/tx_validator_phase1/src/tx_validator_phase1.rs index b2e793e4..9d084009 100644 --- a/modules/tx_validator_phase1/src/tx_validator_phase1.rs +++ b/modules/tx_validator_phase1/src/tx_validator_phase1.rs @@ -4,9 +4,7 @@ mod state; use acropolis_common::{ - messages::{ - CardanoMessage, Message, ProtocolParamsMessage, RawTxsMessage - }, + messages::{CardanoMessage, Message, ProtocolParamsMessage, RawTxsMessage}, *, }; @@ -15,11 +13,11 @@ use acropolis_codec::map_parameters; use caryatid_sdk::{module, Context, Module, Subscription}; use std::{clone::Clone, sync::Arc}; +use crate::state::State; +use acropolis_common::validation::ValidationStatus; use anyhow::{anyhow, bail, Result}; use config::Config; use tracing::{error, info}; -use acropolis_common::validation::ValidationStatus; -use crate::state::State; //mod utxo_registry; //use crate::utxo_registry::UTxORegistry; @@ -27,8 +25,10 @@ const DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC: (&str, &str) = ("transactions-subscribe-topic", "cardano.txs"); const DEFAULT_PROTOCOL_PARAMETERS_TOPIC: (&str, &str) = ("parameters-topic", "cardano.protocol.parameters"); -const DEFAULT_VALIDATION_RESULT_TOPIC: (&str, &str) = - ("publish-valiadtion-result-topic", "cardano.validation.tx-phase-1"); +const DEFAULT_VALIDATION_RESULT_TOPIC: (&str, &str) = ( + "publish-valiadtion-result-topic", + "cardano.validation.tx-phase-1", +); const DEFAULT_NETWORK_NAME: (&str, &str) = ("network-name", "mainnet"); //const CIP25_METADATA_LABEL: u64 = 721; @@ -92,9 +92,7 @@ impl TxValidatorPhase1 { Message::Cardano((blk, CardanoMessage::ReceivedTxs(tx))) => { Ok((blk.clone(), tx.clone())) } - msg => Err(anyhow!( - "Unexpected message {msg:?} for transaction topic" - )), + msg => Err(anyhow!("Unexpected message {msg:?} for transaction topic")), } } @@ -106,7 +104,7 @@ impl TxValidatorPhase1 { if let ValidationStatus::NoGo(res) = &result { error!("Cannot validate transaction: {:?}", res); } - + let packed_message = Arc::new(Message::Cardano(( block.clone(), CardanoMessage::BlockValidation(result), @@ -124,10 +122,11 @@ impl TxValidatorPhase1 { Ok(()) } - async fn run(state: &mut State, - mut _gen: Box>, - mut txs: Box>, - mut params: Box> + async fn run( + state: &mut State, + mut _gen: Box>, + mut txs: Box>, + mut params: Box>, ) -> Result<()> { loop { let (trx_b, trx) = Self::read_transactions(&mut txs).await?; @@ -156,7 +155,8 @@ impl TxValidatorPhase1 { context.clone().run(async move { let mut state = State::new(config.clone()); TxValidatorPhase1::run(&mut state, gen_sub, txs_sub, params_sub) - .await.unwrap_or_else(|e| error!("TX validator failed: {e}")); + .await + .unwrap_or_else(|e| error!("TX validator failed: {e}")); }); Ok(()) diff --git a/processes/omnibus/src/main.rs b/processes/omnibus/src/main.rs index 19e9a7f3..94c2f473 100644 --- a/processes/omnibus/src/main.rs +++ b/processes/omnibus/src/main.rs @@ -31,8 +31,8 @@ use acropolis_module_spdd_state::SPDDState; use acropolis_module_spo_state::SPOState; use acropolis_module_stake_delta_filter::StakeDeltaFilter; use acropolis_module_tx_unpacker::TxUnpacker; -use acropolis_module_utxo_state::UTXOState; use acropolis_module_tx_validator_phase1::TxValidatorPhase1; +use acropolis_module_utxo_state::UTXOState; use caryatid_module_clock::Clock; use caryatid_module_rest_server::RESTServer; From d77693dc953e928f884c856ac8ff6653c41a0d4d Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 21 Nov 2025 17:34:08 +0100 Subject: [PATCH 06/18] feat: added custom macros for transaction validation test cases --- Cargo.lock | 36 +++++++++++++++ modules/tx_unpacker/Cargo.toml | 3 ++ modules/tx_unpacker/src/test_utils.rs | 46 +++++++++++++++++++ modules/tx_unpacker/src/tx_unpacker.rs | 1 + .../tx_unpacker/src/validations/shelley.rs | 36 ++++++--------- .../context.json | 3 ++ .../invalid-ttl/context.json | 3 ++ .../invalid-ttl/tx.cbor | 0 .../tx.cbor | 0 9 files changed, 105 insertions(+), 23 deletions(-) create mode 100644 modules/tx_unpacker/src/test_utils.rs create mode 100644 modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json create mode 100644 modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/context.json rename modules/tx_unpacker/{src/validations => }/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/tx.cbor (100%) rename modules/tx_unpacker/{src/validations => }/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor (100%) diff --git a/Cargo.lock b/Cargo.lock index 65a379d7..cba141c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -455,6 +455,9 @@ dependencies = [ "futures", "hex", "pallas 0.33.0", + "serde", + "serde_json", + "test-case", "tracing", ] @@ -6248,6 +6251,39 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", + "test-case-core", +] + [[package]] name = "textwrap" version = "0.11.0" diff --git a/modules/tx_unpacker/Cargo.toml b/modules/tx_unpacker/Cargo.toml index 620d3641..5e890efb 100644 --- a/modules/tx_unpacker/Cargo.toml +++ b/modules/tx_unpacker/Cargo.toml @@ -20,6 +20,9 @@ futures = "0.3.31" hex = { workspace = true } pallas = { workspace = true } tracing = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +test-case = "3.3.1" [lib] path = "src/tx_unpacker.rs" diff --git a/modules/tx_unpacker/src/test_utils.rs b/modules/tx_unpacker/src/test_utils.rs new file mode 100644 index 00000000..04172968 --- /dev/null +++ b/modules/tx_unpacker/src/test_utils.rs @@ -0,0 +1,46 @@ +use acropolis_common::Slot; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct TestContext { + pub current_slot: Slot, +} + +#[macro_export] +macro_rules! include_cbor { + ($filepath:expr) => { + hex::decode(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/transactions/", + $filepath, + ))) + .expect(concat!("invalid cbor file: ", $filepath)) + }; +} + +#[macro_export] +macro_rules! include_context { + ($filepath:expr) => { + serde_json::from_str::<$crate::test_utils::TestContext>(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/transactions/", + $filepath, + ))) + .expect(concat!("invalid context file: ", $filepath)) + }; +} + +#[macro_export] +macro_rules! validation_fixture { + ($hash:literal) => { + ( + $crate::include_context!(concat!($hash, "/context.json")), + $crate::include_cbor!(concat!($hash, "/tx.cbor")), + ) + }; + ($hash:literal, $variant:literal) => { + ( + $crate::include_context!(concat!($hash, "/", $variant, "/context.json")), + $crate::include_cbor!(concat!($hash, "/", $variant, "/tx.cbor")), + ) + }; +} diff --git a/modules/tx_unpacker/src/tx_unpacker.rs b/modules/tx_unpacker/src/tx_unpacker.rs index 0f697a8b..b1a92158 100644 --- a/modules/tx_unpacker/src/tx_unpacker.rs +++ b/modules/tx_unpacker/src/tx_unpacker.rs @@ -22,6 +22,7 @@ use tracing::{debug, error, info, info_span, Instrument}; mod utxo_registry; mod validations; use crate::utxo_registry::UTxORegistry; +mod test_utils; const DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC: &str = "cardano.txs"; const DEFAULT_GENESIS_SUBSCRIBE_TOPIC: &str = "cardano.genesis.utxos"; diff --git a/modules/tx_unpacker/src/validations/shelley.rs b/modules/tx_unpacker/src/validations/shelley.rs index 29e1bcd1..74fe2a32 100644 --- a/modules/tx_unpacker/src/validations/shelley.rs +++ b/modules/tx_unpacker/src/validations/shelley.rs @@ -50,31 +50,21 @@ pub fn validate_time_to_live( #[cfg(test)] mod tests { use super::*; + use crate::{test_utils::TestContext, validation_fixture}; use pallas::{codec, ledger::primitives::alonzo::MintedTx as AlonzoMintedTx}; + use test_case::test_case; - #[test] - fn test_ttl() { - let cbor_bytes = hex::decode(include_str!("tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor")).unwrap(); - let mtx = codec::minicbor::decode::(&cbor_bytes).unwrap(); + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e") => + matches Ok(()); + )] + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "invalid-ttl") => + matches Err(TransactionValidationError::ExpiredUTxO { ttl: 7084747, current_slot: 7084748 }); + )] + fn shelley_test( + (ctx, raw_tx): (TestContext, Vec), + ) -> Result<(), TransactionValidationError> { + let mtx = codec::minicbor::decode::(&raw_tx).unwrap(); let metx = MultiEraTx::from_alonzo_compatible(&mtx, PallasEra::Shelley); - - let result = validate_shelley_tx(&metx, 7084748); - assert!(result.is_ok()); - } - - #[test] - fn test_invalid_ttl() { - let cbor_bytes = hex::decode(include_str!("tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/tx.cbor")).unwrap(); - let mtx = codec::minicbor::decode::(&cbor_bytes).unwrap(); - let metx = MultiEraTx::from_alonzo_compatible(&mtx, PallasEra::Shelley); - - let result = validate_shelley_tx(&metx, 7084748); - assert_eq!( - result.err().unwrap(), - TransactionValidationError::ExpiredUTxO { - ttl: 7084747, - current_slot: 7084748 - } - ); + validate_shelley_tx(&metx, ctx.current_slot) } } diff --git a/modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json b/modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json new file mode 100644 index 00000000..a769f48f --- /dev/null +++ b/modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json @@ -0,0 +1,3 @@ +{ + "current_slot": 7084748 +} diff --git a/modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/context.json b/modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/context.json new file mode 100644 index 00000000..a769f48f --- /dev/null +++ b/modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/context.json @@ -0,0 +1,3 @@ +{ + "current_slot": 7084748 +} diff --git a/modules/tx_unpacker/src/validations/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/tx.cbor b/modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/tx.cbor similarity index 100% rename from modules/tx_unpacker/src/validations/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/tx.cbor rename to modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/tx.cbor diff --git a/modules/tx_unpacker/src/validations/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor b/modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor similarity index 100% rename from modules/tx_unpacker/src/validations/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor rename to modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor From 51d843a7f1df6739fd41537e29fc7e4c797404cb Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Mon, 24 Nov 2025 13:29:31 +0100 Subject: [PATCH 07/18] refactor: reorganize tests folder structure --- modules/tx_unpacker/src/test_utils.rs | 8 ++++---- modules/tx_unpacker/src/validations/shelley.rs | 2 +- .../context.json | 0 .../tx.cbor | 0 .../wrong_ttl.cbor} | 0 .../invalid-ttl/context.json | 3 --- 6 files changed, 5 insertions(+), 8 deletions(-) rename modules/tx_unpacker/tests/data/{transactions => }/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json (100%) rename modules/tx_unpacker/tests/data/{transactions => }/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor (100%) rename modules/tx_unpacker/tests/data/{transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/tx.cbor => 20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/wrong_ttl.cbor} (100%) delete mode 100644 modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/context.json diff --git a/modules/tx_unpacker/src/test_utils.rs b/modules/tx_unpacker/src/test_utils.rs index 04172968..e6c73fcd 100644 --- a/modules/tx_unpacker/src/test_utils.rs +++ b/modules/tx_unpacker/src/test_utils.rs @@ -10,7 +10,7 @@ macro_rules! include_cbor { ($filepath:expr) => { hex::decode(include_str!(concat!( env!("CARGO_MANIFEST_DIR"), - "/tests/data/transactions/", + "/tests/data/", $filepath, ))) .expect(concat!("invalid cbor file: ", $filepath)) @@ -22,7 +22,7 @@ macro_rules! include_context { ($filepath:expr) => { serde_json::from_str::<$crate::test_utils::TestContext>(include_str!(concat!( env!("CARGO_MANIFEST_DIR"), - "/tests/data/transactions/", + "/tests/data/", $filepath, ))) .expect(concat!("invalid context file: ", $filepath)) @@ -39,8 +39,8 @@ macro_rules! validation_fixture { }; ($hash:literal, $variant:literal) => { ( - $crate::include_context!(concat!($hash, "/", $variant, "/context.json")), - $crate::include_cbor!(concat!($hash, "/", $variant, "/tx.cbor")), + $crate::include_context!(concat!($hash, "/", "/context.json")), + $crate::include_cbor!(concat!($hash, "/", $variant, ".cbor")), ) }; } diff --git a/modules/tx_unpacker/src/validations/shelley.rs b/modules/tx_unpacker/src/validations/shelley.rs index 74fe2a32..dd42fc22 100644 --- a/modules/tx_unpacker/src/validations/shelley.rs +++ b/modules/tx_unpacker/src/validations/shelley.rs @@ -57,7 +57,7 @@ mod tests { #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e") => matches Ok(()); )] - #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "invalid-ttl") => + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "wrong_ttl") => matches Err(TransactionValidationError::ExpiredUTxO { ttl: 7084747, current_slot: 7084748 }); )] fn shelley_test( diff --git a/modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json similarity index 100% rename from modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json rename to modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json diff --git a/modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor similarity index 100% rename from modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor rename to modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/tx.cbor diff --git a/modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/tx.cbor b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/wrong_ttl.cbor similarity index 100% rename from modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/tx.cbor rename to modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/wrong_ttl.cbor diff --git a/modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/context.json b/modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/context.json deleted file mode 100644 index a769f48f..00000000 --- a/modules/tx_unpacker/tests/data/transactions/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/invalid-ttl/context.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "current_slot": 7084748 -} From f89bbe57fcda4aa82fd9c92abba8092b30f9b2c2 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Mon, 24 Nov 2025 16:36:54 +0100 Subject: [PATCH 08/18] refactor: add 2 validation rules for shelley --- common/src/protocol_params.rs | 17 +++++ common/src/validation.rs | 4 + .../tx_unpacker/src/validations/shelley.rs | 73 ++++++++++++++----- 3 files changed, 77 insertions(+), 17 deletions(-) diff --git a/common/src/protocol_params.rs b/common/src/protocol_params.rs index cfc07d10..c55b28b5 100644 --- a/common/src/protocol_params.rs +++ b/common/src/protocol_params.rs @@ -21,6 +21,17 @@ pub struct ProtocolParams { pub conway: Option, } +impl ProtocolParams { + /// Calculate Transaction's Mininum required fee for shelley Era + /// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Tx.hs#L254 + pub fn shelley_min_fee(&self, tx_bytes: u64) -> Result { + self.shelley + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Shelley params are not set")) + .map(|shelley_params| shelley_params.min_fee(tx_bytes)) + } +} + // // Byron protocol parameters // @@ -134,6 +145,12 @@ pub struct ShelleyParams { pub gen_delegs: HashMap, } +impl ShelleyParams { + pub fn min_fee(&self, tx_bytes: u64) -> u64 { + (tx_bytes * self.protocol_params.minfee_a as u64) + (self.protocol_params.minfee_b as u64) + } +} + #[serde_as] #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] diff --git a/common/src/validation.rs b/common/src/validation.rs index 809d8875..ed5fe361 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -99,6 +99,10 @@ pub enum TransactionValidationError { /// **Cause:** The transaction size is too big. #[error("Max tx size: supplied={supplied}, max={max}")] MaxTxSizeUTxO { supplied: u32, max: u32 }, + + /// **Cause:** Other errors (e.g. Invalid shelley params) + #[error("{0}")] + Other(String), } /// Validation error diff --git a/modules/tx_unpacker/src/validations/shelley.rs b/modules/tx_unpacker/src/validations/shelley.rs index dd42fc22..23f689d3 100644 --- a/modules/tx_unpacker/src/validations/shelley.rs +++ b/modules/tx_unpacker/src/validations/shelley.rs @@ -1,27 +1,36 @@ //! Shelley era transaction validation //! Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L343 -use acropolis_common::{validation::TransactionValidationError, Era}; -use pallas::ledger::{ - primitives::alonzo, - traverse::{Era as PallasEra, MultiEraTx}, +use acropolis_common::{ + protocol_params::ShelleyParams, validation::TransactionValidationError, Era, }; +use pallas::{codec, ledger::primitives::alonzo}; + +pub fn get_alonzo_comp_tx_size(mtx: &alonzo::MintedTx) -> u32 { + match &mtx.auxiliary_data { + codec::utils::Nullable::Some(aux_data) => { + (aux_data.raw_cbor().len() + + mtx.transaction_body.raw_cbor().len() + + mtx.transaction_witness_set.raw_cbor().len()) as u32 + } + _ => { + (mtx.transaction_body.raw_cbor().len() + mtx.transaction_witness_set.raw_cbor().len()) + as u32 + } + } +} pub fn validate_shelley_tx( - tx: &MultiEraTx, + mtx: &alonzo::MintedTx, + shelley_params: &ShelleyParams, current_slot: u64, ) -> Result<(), TransactionValidationError> { - let tx = match tx { - MultiEraTx::AlonzoCompatible(tx, PallasEra::Shelley) => tx, - _ => { - return Err(TransactionValidationError::MalformedTransaction { - era: Era::Shelley, - reason: "Transaction is not Shelley compatible".to_string(), - }) - } - }; + let tx_size = get_alonzo_comp_tx_size(mtx) as u64; + let transaction_body = &mtx.transaction_body; - validate_time_to_live(tx, current_slot)?; + validate_time_to_live(mtx, current_slot)?; + validate_input_set_empty_utxo(transaction_body)?; + validate_fee_too_small_utxo(transaction_body, tx_size, shelley_params)?; Ok(()) } @@ -47,6 +56,37 @@ pub fn validate_time_to_live( } } +/// Validate every transaction must consume at least one UTxO +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L435 +pub fn validate_input_set_empty_utxo( + transaction_body: &alonzo::TransactionBody, +) -> Result<(), TransactionValidationError> { + if transaction_body.inputs.is_empty() { + Err(TransactionValidationError::InputSetEmptyUTxO) + } else { + Ok(()) + } +} + +/// Validate every transaction has minimum fee required +/// Fee calculation: +/// minFee = (tx_size_in_bytes * min_a) + min_b + ref_script_fee (this is after Alonzo Era) +pub fn validate_fee_too_small_utxo( + transaction_body: &alonzo::TransactionBody, + tx_size: u64, + shelley_params: &ShelleyParams, +) -> Result<(), TransactionValidationError> { + let min_fee = shelley_params.min_fee(tx_size); + if transaction_body.fee < min_fee { + Err(TransactionValidationError::FeeTooSmallUTxO { + supplied: transaction_body.fee, + required: min_fee, + }) + } else { + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -64,7 +104,6 @@ mod tests { (ctx, raw_tx): (TestContext, Vec), ) -> Result<(), TransactionValidationError> { let mtx = codec::minicbor::decode::(&raw_tx).unwrap(); - let metx = MultiEraTx::from_alonzo_compatible(&mtx, PallasEra::Shelley); - validate_shelley_tx(&metx, ctx.current_slot) + validate_shelley_tx(&mtx, &ctx.shelley_params, ctx.current_slot) } } From 7705f300d4b4b579faed8ae64484bec4bf805d1c Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Tue, 25 Nov 2025 15:11:02 +0100 Subject: [PATCH 09/18] refactor: add shelley params to test context --- modules/tx_unpacker/src/test_utils.rs | 3 +- .../context.json | 40 ++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/modules/tx_unpacker/src/test_utils.rs b/modules/tx_unpacker/src/test_utils.rs index e6c73fcd..663428b8 100644 --- a/modules/tx_unpacker/src/test_utils.rs +++ b/modules/tx_unpacker/src/test_utils.rs @@ -1,7 +1,8 @@ -use acropolis_common::Slot; +use acropolis_common::{protocol_params::ShelleyParams, Slot}; #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct TestContext { + pub shelley_params: ShelleyParams, pub current_slot: Slot, } diff --git a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json index a769f48f..387fdb9d 100644 --- a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json +++ b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json @@ -1,3 +1,41 @@ { - "current_slot": 7084748 + "current_slot": 7084748, + "shelley_params": { + "activeSlotsCoeff": 0.05, + "epochLength": 432000, + "maxKesEvolutions": 62, + "maxLovelaceSupply": 45000000000000000, + "networkId": "Mainnet", + "networkMagic": 764824073, + "protocolParams": { + "protocolVersion": { + "minor": 0, + "major": 2 + }, + "maxTxSize": 16384, + "maxBlockBodySize": 65536, + "maxBlockHeaderSize": 1100, + "keyDeposit": 2000000, + "minUTxOValue": 1000000, + "minFeeA": 44, + "minFeeB": 155381, + "poolDeposit": 500000000, + "nOpt": 150, + "minPoolCost": 340000000, + "eMax": 18, + "extraEntropy": { + "tag": "NeutralNonce" + }, + "decentralisationParam": 1, + "rho": 0.003, + "tau": 0.2, + "a0": 0.3 + }, + "securityParam": 2160, + "slotLength": 1, + "slotsPerKesPeriod": 129600, + "systemStart": "2017-09-23T21:44:51Z", + "updateQuorum": 5, + "genDelegs": {} + } } From 4a1cb365f2a37d36824de8c3ddba38a91f4fd409 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 26 Nov 2025 17:21:07 +0100 Subject: [PATCH 10/18] wip: parsing pallas tx --- codec/src/map_parameters.rs | 13 +- common/src/types.rs | 39 +----- common/src/validation.rs | 9 +- modules/tx_unpacker/src/utxo_registry.rs | 24 ++-- .../tx_unpacker/src/validations/shelley.rs | 117 ++++++++++++++++-- 5 files changed, 139 insertions(+), 63 deletions(-) diff --git a/codec/src/map_parameters.rs b/codec/src/map_parameters.rs index acf8f5b6..4d34c849 100644 --- a/codec/src/map_parameters.rs +++ b/codec/src/map_parameters.rs @@ -12,7 +12,6 @@ use pallas::ledger::{ *, }; -use crate::map_parameters; use acropolis_common::hash::Hash; use acropolis_common::validation::ValidationError; use acropolis_common::{ @@ -23,7 +22,6 @@ use acropolis_common::{ use pallas_primitives::conway::PseudoScript; use pallas_traverse::{MultiEraInput, MultiEraOutput, MultiEraTx}; use std::collections::{HashMap, HashSet}; -use tracing::error; /// Map Pallas Network to our NetworkId pub fn map_network(network: addresses::Network) -> Result { @@ -997,15 +995,16 @@ pub fn map_transaction_refs( (ref_inps, ref_outs) } +#[allow(clippy::type_complexity)] pub fn map_one_transaction( block_number: u32, tx_index: u16, tx: &MultiEraTx, ) -> ( - Vec, - Vec<(TxOutRef, TxOutput)>, - u128, - Vec, + Vec, // inputs + Vec<(TxOutRef, TxOutput)>, // outputs + u128, // total + Vec, // errors ) { let Ok(tx_hash) = tx.hash().to_vec().try_into() else { return ( @@ -1065,7 +1064,7 @@ pub fn map_one_transaction( Err(e) => { errors.push(ValidationError::MalformedTransaction( tx_index, - format!("Output {index} in tx ignored: {e}"), + format!("Output {index} has been ignored: {e}"), )); } }, diff --git a/common/src/types.rs b/common/src/types.rs index d383406d..3fdf44d2 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -724,39 +724,6 @@ impl fmt::Display for UTxOIdentifier { } } -#[derive( - Debug, - Clone, - serde::Serialize, - serde::Deserialize, - minicbor::Encode, - minicbor::Decode, - PartialEq, - Eq, -)] -pub struct UTxOIdentifierSet(#[n(0)] pub HashSet); - -impl fmt::Display for UTxOIdentifierSet { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let items: Vec = self.0.iter().map(|id| id.to_string()).collect(); - write!(f, "[{}]", items.join(", ")) - } -} - -impl From> for UTxOIdentifierSet { - fn from(set: HashSet) -> Self { - UTxOIdentifierSet(set) - } -} - -impl std::ops::Deref for UTxOIdentifierSet { - type Target = HashSet; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - // Full TxOutRef stored in UTxORegistry for UTxOIdentifier lookups #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct TxOutRef { @@ -773,6 +740,12 @@ impl TxOutRef { } } +impl Display for TxOutRef { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}#{}", self.tx_hash, self.output_index) + } +} + /// Slot pub type Slot = u64; diff --git a/common/src/validation.rs b/common/src/validation.rs index ed5fe361..7fe6443f 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -9,7 +9,7 @@ use thiserror::Error; use crate::{ protocol_params::Nonce, Address, Era, GenesisKeyhash, Lovelace, NetworkId, PoolId, Slot, - StakeAddress, TxOutput, UTxOIdentifierSet, Value, VrfKeyHash, + StakeAddress, TxOutRef, TxOutput, Value, VrfKeyHash, }; /// Transaction Validation Error @@ -52,8 +52,11 @@ pub enum TransactionValidationError { }, /// **Cause:** Some of transaction inputs are not in current UTxOs set. - #[error("Bad inputs: bad_inputs={bad_inputs}")] - BadInputsUTxO { bad_inputs: UTxOIdentifierSet }, + #[error( + "Bad inputs: bad_inputs=[{}]", + bad_inputs.iter().map(|t| t.to_string()).collect::>().join(", ") + )] + BadInputsUTxO { bad_inputs: Vec }, /// **Cause:** Some of transaction outputs are on a different network than the expected one. #[error( diff --git a/modules/tx_unpacker/src/utxo_registry.rs b/modules/tx_unpacker/src/utxo_registry.rs index bdd488d0..c4f8d785 100644 --- a/modules/tx_unpacker/src/utxo_registry.rs +++ b/modules/tx_unpacker/src/utxo_registry.rs @@ -150,6 +150,16 @@ impl UTxORegistry { } } + /// Lookup a TxOutRef and return its identifier + pub fn lookup_by_hash(&self, tx_ref: TxOutRef) -> Result { + self.live_map.get(&tx_ref).copied().ok_or_else(|| { + anyhow::anyhow!( + "TxHash not found or already spent: {:?}", + hex::encode(tx_ref.tx_hash) + ) + }) + } + /// Rollback to block N-1 pub fn rollback_before(&mut self, block_number: u32) -> Result<(), String> { // Remove tx ouputs created at or after rollback block @@ -170,23 +180,11 @@ impl UTxORegistry { #[cfg(test)] mod tests { use crate::utxo_registry::UTxORegistry; - use acropolis_common::{params::SECURITY_PARAMETER_K, TxHash, TxIdentifier, TxOutRef}; - use anyhow::Result; + use acropolis_common::{params::SECURITY_PARAMETER_K, TxHash, TxOutRef}; fn make_hash(byte: u8) -> TxHash { TxHash::new([byte; 32]) } - impl UTxORegistry { - /// Lookup unspent tx output - pub fn lookup_by_hash(&self, tx_ref: TxOutRef) -> Result { - self.live_map.get(&tx_ref).copied().ok_or_else(|| { - anyhow::anyhow!( - "TxHash not found or already spent: {:?}", - hex::encode(tx_ref.tx_hash) - ) - }) - } - } #[test] fn add_and_lookup() { diff --git a/modules/tx_unpacker/src/validations/shelley.rs b/modules/tx_unpacker/src/validations/shelley.rs index 23f689d3..40bba252 100644 --- a/modules/tx_unpacker/src/validations/shelley.rs +++ b/modules/tx_unpacker/src/validations/shelley.rs @@ -1,14 +1,24 @@ //! Shelley era transaction validation //! Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L343 +use acropolis_codec; use acropolis_common::{ - protocol_params::ShelleyParams, validation::TransactionValidationError, Era, + protocol_params::ShelleyParams, validation::TransactionValidationError, Address, Era, + NetworkId, TxIdentifier, TxOutRef, +}; +use anyhow::Result; +use pallas::{ + codec as pallas_codec, + ledger::{ + addresses::Address as PallasAddress, + primitives::alonzo, + traverse::{Era as PallasEra, MultiEraTx}, + }, }; -use pallas::{codec, ledger::primitives::alonzo}; pub fn get_alonzo_comp_tx_size(mtx: &alonzo::MintedTx) -> u32 { match &mtx.auxiliary_data { - codec::utils::Nullable::Some(aux_data) => { + pallas_codec::utils::Nullable::Some(aux_data) => { (aux_data.raw_cbor().len() + mtx.transaction_body.raw_cbor().len() + mtx.transaction_witness_set.raw_cbor().len()) as u32 @@ -21,13 +31,39 @@ pub fn get_alonzo_comp_tx_size(mtx: &alonzo::MintedTx) -> u32 { } pub fn validate_shelley_tx( - mtx: &alonzo::MintedTx, + tx: &MultiEraTx, + block_number: u32, + tx_index: u16, shelley_params: &ShelleyParams, current_slot: u64, ) -> Result<(), TransactionValidationError> { - let tx_size = get_alonzo_comp_tx_size(mtx) as u64; + let tx_size = tx.size() as u64; + + let mtx = match tx { + MultiEraTx::AlonzoCompatible(mtx, PallasEra::Shelley) => mtx, + _ => { + return Err(TransactionValidationError::MalformedTransaction { + era: Era::Shelley, + reason: "Not a Shelley transaction".to_string(), + }) + } + }; let transaction_body = &mtx.transaction_body; + // map pallas transaction to acropolis transaction + let (inputs, outputs, _, errors) = + acropolis_codec::map_parameters::map_one_transaction(block_number, tx_index, &tx); + + if !errors.is_empty() { + return Err(TransactionValidationError::MalformedTransaction { + era: Era::Shelley, + reason: format!( + "Errors: {}", + errors.iter().map(|e| e.to_string()).collect::>().join(", ") + ), + }); + } + validate_time_to_live(mtx, current_slot)?; validate_input_set_empty_utxo(transaction_body)?; validate_fee_too_small_utxo(transaction_body, tx_size, shelley_params)?; @@ -71,6 +107,7 @@ pub fn validate_input_set_empty_utxo( /// Validate every transaction has minimum fee required /// Fee calculation: /// minFee = (tx_size_in_bytes * min_a) + min_b + ref_script_fee (this is after Alonzo Era) +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L447 pub fn validate_fee_too_small_utxo( transaction_body: &alonzo::TransactionBody, tx_size: u64, @@ -87,11 +124,77 @@ pub fn validate_fee_too_small_utxo( } } +/// Validate every transaction's input exists in the current UTxO set. +/// This prevents double spending. +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L468 +pub fn validate_bad_inputs_utxo( + transaction_body: &alonzo::TransactionBody, + lookup_by_hash: F, +) -> Result<(), TransactionValidationError> +where + F: Fn(TxOutRef) -> Result, +{ + let bad_inputs = transaction_body + .inputs + .iter() + .filter_map(|input| { + let tx_ref = TxOutRef::new((*input.transaction_id).into(), input.index as u16); + lookup_by_hash(tx_ref).is_err().then_some(tx_ref) + }) + .collect::>(); + + if !bad_inputs.is_empty() { + Err(TransactionValidationError::BadInputsUTxO { bad_inputs }) + } else { + Ok(()) + } +} + +/// Validate every output address match the network +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L481 +pub fn validateWrongNetwork( + transaction_body: &alonzo::TransactionBody, + network_id: NetworkId, +) -> Result<(), TransactionValidationError> { + let mut wrong_addresses = Vec::new(); + for output in transaction_body.outputs.iter() { + let pallas_address = PallasAddress::from_bytes(output.address.as_ref()).map_err(|_| { + TransactionValidationError::MalformedTransaction { + era: Era::Shelley, + reason: "Malformed address".to_string(), + } + })?; + let address = map_address(&pallas_address).map_err(|e| { + TransactionValidationError::MalformedTransaction { + era: Era::Shelley, + reason: format!("Invalid address: {}", e), + } + })?; + let address_network = match address { + Address::Shelley(sa) => sa.network, + _ => { + return Err(TransactionValidationError::MalformedTransaction { + era: Era::Shelley, + reason: format!( + "Not a Shelley Address: {}", + address.to_string().unwrap_or("Invalid Address format".to_string()) + ), + }) + } + }; + if address_network != network_id { + wrong_addresses.push(address); + } + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; use crate::{test_utils::TestContext, validation_fixture}; - use pallas::{codec, ledger::primitives::alonzo::MintedTx as AlonzoMintedTx}; + use pallas::{codec as pallas_codec, ledger::primitives::alonzo::MintedTx as AlonzoMintedTx}; use test_case::test_case; #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e") => @@ -103,7 +206,7 @@ mod tests { fn shelley_test( (ctx, raw_tx): (TestContext, Vec), ) -> Result<(), TransactionValidationError> { - let mtx = codec::minicbor::decode::(&raw_tx).unwrap(); + let mtx = pallas_codec::minicbor::decode::(&raw_tx).unwrap(); validate_shelley_tx(&mtx, &ctx.shelley_params, ctx.current_slot) } } From 8e13171a73192764ddfc22026c2e1a43f091879e Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Thu, 27 Nov 2025 18:43:59 +0100 Subject: [PATCH 11/18] feat: add more shelley era utxo rules check --- Cargo.lock | 2 +- common/src/protocol_params.rs | 7 +- common/src/types.rs | 1 + common/src/validation.rs | 68 ++-- modules/tx_unpacker/src/test_utils.rs | 33 +- .../tx_unpacker/src/validations/shelley.rs | 212 ------------ .../src/validations/shelley/mod.rs | 1 + .../src/validations/shelley/utxo.rs | 326 ++++++++++++++++++ .../context.json | 8 +- 9 files changed, 406 insertions(+), 252 deletions(-) delete mode 100644 modules/tx_unpacker/src/validations/shelley.rs create mode 100644 modules/tx_unpacker/src/validations/shelley/mod.rs create mode 100644 modules/tx_unpacker/src/validations/shelley/utxo.rs diff --git a/Cargo.lock b/Cargo.lock index 934f2e32..74880779 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7524,7 +7524,7 @@ dependencies = [ "futures", "http 1.3.1", "http-body-util", - "hyper 1.7.0", + "hyper 1.8.1", "hyper-util", "log", "once_cell", diff --git a/common/src/protocol_params.rs b/common/src/protocol_params.rs index c55b28b5..dc6b6712 100644 --- a/common/src/protocol_params.rs +++ b/common/src/protocol_params.rs @@ -24,7 +24,7 @@ pub struct ProtocolParams { impl ProtocolParams { /// Calculate Transaction's Mininum required fee for shelley Era /// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Tx.hs#L254 - pub fn shelley_min_fee(&self, tx_bytes: u64) -> Result { + pub fn shelley_min_fee(&self, tx_bytes: u32) -> Result { self.shelley .as_ref() .ok_or_else(|| anyhow::anyhow!("Shelley params are not set")) @@ -146,8 +146,9 @@ pub struct ShelleyParams { } impl ShelleyParams { - pub fn min_fee(&self, tx_bytes: u64) -> u64 { - (tx_bytes * self.protocol_params.minfee_a as u64) + (self.protocol_params.minfee_b as u64) + pub fn min_fee(&self, tx_bytes: u32) -> u64 { + (tx_bytes as u64 * self.protocol_params.minfee_a as u64) + + (self.protocol_params.minfee_b as u64) } } diff --git a/common/src/types.rs b/common/src/types.rs index 3fdf44d2..a46c4ead 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -29,6 +29,7 @@ use std::{ /// Network identifier #[derive( Debug, + Copy, Clone, Default, PartialEq, diff --git a/common/src/validation.rs b/common/src/validation.rs index 7fe6443f..46f0a3e0 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -27,14 +27,20 @@ pub enum TransactionValidationError { CborDecodeError(String), /// **Cause**: Transaction is not in correct form. - /// e.g. some field is missing from transaction body, when it is required. - /// Reference: - /// Shelley: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/cddl-files/shelley.cddl - /// Allegra: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/allegra/impl/cddl-files/allegra.cddl - /// Alonzo: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/alonzo/impl/cddl-files/alonzo.cddl #[error("Malformed Transaction: era={era}, reason={reason}")] MalformedTransaction { era: Era, reason: String }, + /// **Cause**: UTXO validation error + #[error("{0}")] + UTxOValidationError(#[from] UTxOValidationError), + + /// **Cause:** Other errors (e.g. Invalid shelley params) + #[error("{0}")] + Other(String), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Error, PartialEq, Eq)] +pub enum UTxOValidationError { /// ------------ Shelley Era Errors ------------ /// **Cause:** The UTXO has expired #[error("Expired UTXO: ttl={ttl}, current_slot={current_slot}")] @@ -52,38 +58,32 @@ pub enum TransactionValidationError { }, /// **Cause:** Some of transaction inputs are not in current UTxOs set. - #[error( - "Bad inputs: bad_inputs=[{}]", - bad_inputs.iter().map(|t| t.to_string()).collect::>().join(", ") - )] - BadInputsUTxO { bad_inputs: Vec }, + #[error("Bad inputs: bad_input={bad_input}, bad_input_index={bad_input_index}")] + BadInputsUTxO { + bad_input: TxOutRef, + bad_input_index: usize, + }, /// **Cause:** Some of transaction outputs are on a different network than the expected one. #[error( - "Wrong network: expected={expected}, wrong_addresses=[{}]", - wrong_addresses - .iter() - .map(|a| a.to_string().unwrap_or_else(|_| "invalid address".to_string())) - .collect::>() - .join(", ") + "Wrong network: expected={expected}, wrong_address={}, output_index={output_index}", + wrong_address.to_string().unwrap_or("Invalid address".to_string()), )] WrongNetwork { expected: NetworkId, - wrong_addresses: Vec
, + wrong_address: Address, + output_index: usize, }, /// **Cause:** Some of withdrawal accounts are on a different network than the expected one. #[error( - "Wrong network withdrawal: expected={expected}, wrong_accounts=[{}]", - wrong_accounts - .iter() - .map(|a| a.to_string().unwrap_or_else(|_| "invalid address".to_string())) - .collect::>() - .join(", ") + "Wrong network withdrawal: expected={expected}, wrong_account={}, withdrawal_index={withdrawal_index}", + wrong_account.to_string().unwrap_or("Invalid stake address".to_string()), )] WrongNetworkWithdrawal { expected: NetworkId, - wrong_accounts: Vec, + wrong_account: StakeAddress, + withdrawal_index: usize, }, /// **Cause:** The value of the UTXO is not conserved. @@ -92,20 +92,22 @@ pub enum TransactionValidationError { ValueNotConservedUTxO { consumed: Value, produced: Value }, /// **Cause:** Some of the outputs don't have minimum required lovelace - #[error("Output too small UTxO: small_outputs={small_outputs:?}")] - OutputTooSmallUTxO { small_outputs: Vec }, - - /// **Cause:** Some of the outputs have boot address (only byron-era) attributes that are too big - #[error("Output boot address attrs too big: large_outputs={large_outputs:?}")] - OutputBootAddrAttrsTooBig { large_outputs: Vec }, + #[error( + "Output too small UTxO: output_index={output_index}, lovelace={lovelace}, required_lovelace={required_lovelace}" + )] + OutputTooSmallUTxO { + output_index: usize, + lovelace: Lovelace, + required_lovelace: Lovelace, + }, /// **Cause:** The transaction size is too big. #[error("Max tx size: supplied={supplied}, max={max}")] MaxTxSizeUTxO { supplied: u32, max: u32 }, - /// **Cause:** Other errors (e.g. Invalid shelley params) - #[error("{0}")] - Other(String), + /// **Cause:** Malformed UTxO + #[error("Malformed UTxO: era={era}, reason={reason}")] + MalformedUTxO { era: Era, reason: String }, } /// Validation error diff --git a/modules/tx_unpacker/src/test_utils.rs b/modules/tx_unpacker/src/test_utils.rs index 663428b8..5193ffee 100644 --- a/modules/tx_unpacker/src/test_utils.rs +++ b/modules/tx_unpacker/src/test_utils.rs @@ -1,11 +1,39 @@ -use acropolis_common::{protocol_params::ShelleyParams, Slot}; +use std::{collections::HashMap, str::FromStr}; + +use acropolis_common::{protocol_params::ShelleyParams, Slot, TxHash, TxIdentifier, TxOutRef}; #[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct TestContextJson { + pub shelley_params: ShelleyParams, + pub current_slot: Slot, + // Vec<((TxHash, TxIndex), (BlockNumber, TxIndex))> + pub utxos: Vec<((String, u16), (u32, u16))>, +} + pub struct TestContext { pub shelley_params: ShelleyParams, pub current_slot: Slot, + pub utxos: HashMap, } +impl From for TestContext { + fn from(json: TestContextJson) -> Self { + Self { + shelley_params: json.shelley_params, + current_slot: json.current_slot, + utxos: json + .utxos + .into_iter() + .map(|((tx_hash, output_index), (block_number, tx_index))| { + ( + TxOutRef::new(TxHash::from_str(&tx_hash).unwrap(), output_index), + TxIdentifier::new(block_number, tx_index), + ) + }) + .collect(), + } + } +} #[macro_export] macro_rules! include_cbor { ($filepath:expr) => { @@ -21,12 +49,13 @@ macro_rules! include_cbor { #[macro_export] macro_rules! include_context { ($filepath:expr) => { - serde_json::from_str::<$crate::test_utils::TestContext>(include_str!(concat!( + serde_json::from_str::<$crate::test_utils::TestContextJson>(include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/tests/data/", $filepath, ))) .expect(concat!("invalid context file: ", $filepath)) + .into() }; } diff --git a/modules/tx_unpacker/src/validations/shelley.rs b/modules/tx_unpacker/src/validations/shelley.rs deleted file mode 100644 index 40bba252..00000000 --- a/modules/tx_unpacker/src/validations/shelley.rs +++ /dev/null @@ -1,212 +0,0 @@ -//! Shelley era transaction validation -//! Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L343 - -use acropolis_codec; -use acropolis_common::{ - protocol_params::ShelleyParams, validation::TransactionValidationError, Address, Era, - NetworkId, TxIdentifier, TxOutRef, -}; -use anyhow::Result; -use pallas::{ - codec as pallas_codec, - ledger::{ - addresses::Address as PallasAddress, - primitives::alonzo, - traverse::{Era as PallasEra, MultiEraTx}, - }, -}; - -pub fn get_alonzo_comp_tx_size(mtx: &alonzo::MintedTx) -> u32 { - match &mtx.auxiliary_data { - pallas_codec::utils::Nullable::Some(aux_data) => { - (aux_data.raw_cbor().len() - + mtx.transaction_body.raw_cbor().len() - + mtx.transaction_witness_set.raw_cbor().len()) as u32 - } - _ => { - (mtx.transaction_body.raw_cbor().len() + mtx.transaction_witness_set.raw_cbor().len()) - as u32 - } - } -} - -pub fn validate_shelley_tx( - tx: &MultiEraTx, - block_number: u32, - tx_index: u16, - shelley_params: &ShelleyParams, - current_slot: u64, -) -> Result<(), TransactionValidationError> { - let tx_size = tx.size() as u64; - - let mtx = match tx { - MultiEraTx::AlonzoCompatible(mtx, PallasEra::Shelley) => mtx, - _ => { - return Err(TransactionValidationError::MalformedTransaction { - era: Era::Shelley, - reason: "Not a Shelley transaction".to_string(), - }) - } - }; - let transaction_body = &mtx.transaction_body; - - // map pallas transaction to acropolis transaction - let (inputs, outputs, _, errors) = - acropolis_codec::map_parameters::map_one_transaction(block_number, tx_index, &tx); - - if !errors.is_empty() { - return Err(TransactionValidationError::MalformedTransaction { - era: Era::Shelley, - reason: format!( - "Errors: {}", - errors.iter().map(|e| e.to_string()).collect::>().join(", ") - ), - }); - } - - validate_time_to_live(mtx, current_slot)?; - validate_input_set_empty_utxo(transaction_body)?; - validate_fee_too_small_utxo(transaction_body, tx_size, shelley_params)?; - Ok(()) -} - -/// Validate transaction's TTL field -/// pass if ttl >= current_slot -/// Reference -/// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L421 -pub fn validate_time_to_live( - tx: &alonzo::MintedTx, - current_slot: u64, -) -> Result<(), TransactionValidationError> { - if let Some(ttl) = tx.transaction_body.ttl { - if ttl >= current_slot { - Ok(()) - } else { - Err(TransactionValidationError::ExpiredUTxO { ttl, current_slot }) - } - } else { - Err(TransactionValidationError::MalformedTransaction { - era: Era::Shelley, - reason: "TTL is missing".to_string(), - }) - } -} - -/// Validate every transaction must consume at least one UTxO -/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L435 -pub fn validate_input_set_empty_utxo( - transaction_body: &alonzo::TransactionBody, -) -> Result<(), TransactionValidationError> { - if transaction_body.inputs.is_empty() { - Err(TransactionValidationError::InputSetEmptyUTxO) - } else { - Ok(()) - } -} - -/// Validate every transaction has minimum fee required -/// Fee calculation: -/// minFee = (tx_size_in_bytes * min_a) + min_b + ref_script_fee (this is after Alonzo Era) -/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L447 -pub fn validate_fee_too_small_utxo( - transaction_body: &alonzo::TransactionBody, - tx_size: u64, - shelley_params: &ShelleyParams, -) -> Result<(), TransactionValidationError> { - let min_fee = shelley_params.min_fee(tx_size); - if transaction_body.fee < min_fee { - Err(TransactionValidationError::FeeTooSmallUTxO { - supplied: transaction_body.fee, - required: min_fee, - }) - } else { - Ok(()) - } -} - -/// Validate every transaction's input exists in the current UTxO set. -/// This prevents double spending. -/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L468 -pub fn validate_bad_inputs_utxo( - transaction_body: &alonzo::TransactionBody, - lookup_by_hash: F, -) -> Result<(), TransactionValidationError> -where - F: Fn(TxOutRef) -> Result, -{ - let bad_inputs = transaction_body - .inputs - .iter() - .filter_map(|input| { - let tx_ref = TxOutRef::new((*input.transaction_id).into(), input.index as u16); - lookup_by_hash(tx_ref).is_err().then_some(tx_ref) - }) - .collect::>(); - - if !bad_inputs.is_empty() { - Err(TransactionValidationError::BadInputsUTxO { bad_inputs }) - } else { - Ok(()) - } -} - -/// Validate every output address match the network -/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L481 -pub fn validateWrongNetwork( - transaction_body: &alonzo::TransactionBody, - network_id: NetworkId, -) -> Result<(), TransactionValidationError> { - let mut wrong_addresses = Vec::new(); - for output in transaction_body.outputs.iter() { - let pallas_address = PallasAddress::from_bytes(output.address.as_ref()).map_err(|_| { - TransactionValidationError::MalformedTransaction { - era: Era::Shelley, - reason: "Malformed address".to_string(), - } - })?; - let address = map_address(&pallas_address).map_err(|e| { - TransactionValidationError::MalformedTransaction { - era: Era::Shelley, - reason: format!("Invalid address: {}", e), - } - })?; - let address_network = match address { - Address::Shelley(sa) => sa.network, - _ => { - return Err(TransactionValidationError::MalformedTransaction { - era: Era::Shelley, - reason: format!( - "Not a Shelley Address: {}", - address.to_string().unwrap_or("Invalid Address format".to_string()) - ), - }) - } - }; - if address_network != network_id { - wrong_addresses.push(address); - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{test_utils::TestContext, validation_fixture}; - use pallas::{codec as pallas_codec, ledger::primitives::alonzo::MintedTx as AlonzoMintedTx}; - use test_case::test_case; - - #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e") => - matches Ok(()); - )] - #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "wrong_ttl") => - matches Err(TransactionValidationError::ExpiredUTxO { ttl: 7084747, current_slot: 7084748 }); - )] - fn shelley_test( - (ctx, raw_tx): (TestContext, Vec), - ) -> Result<(), TransactionValidationError> { - let mtx = pallas_codec::minicbor::decode::(&raw_tx).unwrap(); - validate_shelley_tx(&mtx, &ctx.shelley_params, ctx.current_slot) - } -} diff --git a/modules/tx_unpacker/src/validations/shelley/mod.rs b/modules/tx_unpacker/src/validations/shelley/mod.rs new file mode 100644 index 00000000..5b379e46 --- /dev/null +++ b/modules/tx_unpacker/src/validations/shelley/mod.rs @@ -0,0 +1 @@ +pub mod utxo; diff --git a/modules/tx_unpacker/src/validations/shelley/utxo.rs b/modules/tx_unpacker/src/validations/shelley/utxo.rs new file mode 100644 index 00000000..81545440 --- /dev/null +++ b/modules/tx_unpacker/src/validations/shelley/utxo.rs @@ -0,0 +1,326 @@ +//! Shelley era transaction validation +//! Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L343 + +use acropolis_codec; +use acropolis_common::{ + protocol_params::ShelleyParams, validation::UTxOValidationError, Address, Era, Lovelace, + NetworkId, TxIdentifier, TxOutRef, +}; +use anyhow::Result; +use pallas::{ + codec as pallas_codec, + ledger::{ + addresses::Address as PallasAddress, + primitives::alonzo, + traverse::{Era as PallasEra, MultiEraTx}, + }, +}; + +fn get_alonzo_comp_tx_size(mtx: &alonzo::MintedTx) -> u32 { + match &mtx.auxiliary_data { + pallas_codec::utils::Nullable::Some(aux_data) => { + (aux_data.raw_cbor().len() + + mtx.transaction_body.raw_cbor().len() + + mtx.transaction_witness_set.raw_cbor().len()) as u32 + } + _ => { + (mtx.transaction_body.raw_cbor().len() + mtx.transaction_witness_set.raw_cbor().len()) + as u32 + } + } +} + +fn get_lovelace_from_alonzo_value(val: &alonzo::Value) -> Lovelace { + match val { + alonzo::Value::Coin(res) => *res, + alonzo::Value::Multiasset(res, _) => *res, + } +} + +fn get_value_size_in_bytes(val: &alonzo::Value) -> u64 { + let mut buf = Vec::new(); + let _ = pallas_codec::minicbor::encode(val, &mut buf); + (buf.len() as u64).div_ceil(8) +} + +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/mary/impl/src/Cardano/Ledger/Mary/TxOut.hs#L52 +fn compute_min_lovelace(value: &alonzo::Value, shelley_params: &ShelleyParams) -> Lovelace { + match value { + alonzo::Value::Coin(_) => shelley_params.protocol_params.min_utxo_value, + alonzo::Value::Multiasset(lovelace, _) => { + let utxo_entry_size = 27 + get_value_size_in_bytes(value); + let coins_per_utxo_word = shelley_params.protocol_params.min_utxo_value / 27; + (*lovelace).max(coins_per_utxo_word * utxo_entry_size) + } + } +} + +pub type UTxOValidationResult = Result<(), Box>; + +pub fn validate_shelley_tx( + tx: &MultiEraTx, + shelley_params: &ShelleyParams, + current_slot: u64, + lookup_by_hash: F, +) -> UTxOValidationResult +where + F: Fn(TxOutRef) -> Result, +{ + let network_id = shelley_params.network_id; + let tx_size = tx.size() as u32; + + let mtx = match tx { + MultiEraTx::AlonzoCompatible(mtx, PallasEra::Shelley) => mtx, + _ => { + return Err(Box::new(UTxOValidationError::MalformedUTxO { + era: Era::Shelley, + reason: "Not a Shelley transaction".to_string(), + })); + } + }; + let transaction_body = &mtx.transaction_body; + + validate_time_to_live(mtx, current_slot)?; + validate_input_set_empty_utxo(transaction_body)?; + validate_fee_too_small_utxo(transaction_body, tx_size, shelley_params)?; + validate_bad_inputs_utxo(transaction_body, lookup_by_hash)?; + validate_wrong_network(transaction_body, network_id)?; + validate_wrong_network_withdrawal(transaction_body, network_id)?; + validate_output_too_small_utxo(transaction_body, shelley_params)?; + validate_max_tx_size_utxo(tx_size, shelley_params)?; + Ok(()) +} + +/// Validate transaction's TTL field +/// pass if ttl >= current_slot +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L421 +pub fn validate_time_to_live(tx: &alonzo::MintedTx, current_slot: u64) -> UTxOValidationResult { + if let Some(ttl) = tx.transaction_body.ttl { + if ttl >= current_slot { + Ok(()) + } else { + Err(Box::new(UTxOValidationError::ExpiredUTxO { + ttl, + current_slot, + })) + } + } else { + Err(Box::new(UTxOValidationError::MalformedUTxO { + era: Era::Shelley, + reason: "TTL is missing".to_string(), + })) + } +} + +/// Validate every transaction must consume at least one UTxO +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L435 +pub fn validate_input_set_empty_utxo( + transaction_body: &alonzo::TransactionBody, +) -> UTxOValidationResult { + if transaction_body.inputs.is_empty() { + Err(Box::new(UTxOValidationError::InputSetEmptyUTxO)) + } else { + Ok(()) + } +} + +/// Validate every transaction has minimum fee required +/// Fee calculation: +/// minFee = (tx_size_in_bytes * min_a) + min_b + ref_script_fee (this is after Alonzo Era) +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L447 +pub fn validate_fee_too_small_utxo( + transaction_body: &alonzo::TransactionBody, + tx_size: u32, + shelley_params: &ShelleyParams, +) -> UTxOValidationResult { + let min_fee = shelley_params.min_fee(tx_size); + if transaction_body.fee < min_fee { + Err(Box::new(UTxOValidationError::FeeTooSmallUTxO { + supplied: transaction_body.fee, + required: min_fee, + })) + } else { + Ok(()) + } +} + +/// Validate every transaction's input exists in the current UTxO set. +/// This prevents double spending. +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L468 +pub fn validate_bad_inputs_utxo( + transaction_body: &alonzo::TransactionBody, + lookup_by_hash: F, +) -> UTxOValidationResult +where + F: Fn(TxOutRef) -> Result, +{ + for (index, input) in transaction_body.inputs.iter().enumerate() { + let tx_ref = TxOutRef::new((*input.transaction_id).into(), input.index as u16); + if lookup_by_hash(tx_ref).is_err() { + return Err(Box::new(UTxOValidationError::BadInputsUTxO { + bad_input: tx_ref, + bad_input_index: index, + })); + } + } + Ok(()) +} + +/// Validate every output address match the network +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L481 +pub fn validate_wrong_network( + transaction_body: &alonzo::TransactionBody, + network_id: NetworkId, +) -> UTxOValidationResult { + for (index, output) in transaction_body.outputs.iter().enumerate() { + let pallas_address = PallasAddress::from_bytes(output.address.as_ref()).map_err(|_| { + Box::new(UTxOValidationError::MalformedUTxO { + era: Era::Shelley, + reason: format!("Malformed address at output {index}"), + }) + })?; + + let address = + acropolis_codec::map_parameters::map_address(&pallas_address).map_err(|e| { + Box::new(UTxOValidationError::MalformedUTxO { + era: Era::Shelley, + reason: format!("Invalid address at output {index}: {}", e), + }) + })?; + + let address = match address { + Address::Shelley(shelley_address) => shelley_address, + _ => { + return Err(Box::new(UTxOValidationError::MalformedUTxO { + era: Era::Shelley, + reason: format!("Not a Shelley Address at output {index}"), + })) + } + }; + if address.network != network_id { + return Err(Box::new(UTxOValidationError::WrongNetwork { + expected: network_id, + wrong_address: Address::Shelley(address), + output_index: index, + })); + } + } + + Ok(()) +} + +/// Validate every withdrawal account addresses match the network +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L497 +pub fn validate_wrong_network_withdrawal( + transaction_body: &alonzo::TransactionBody, + network_id: NetworkId, +) -> UTxOValidationResult { + let Some(withdrawals) = transaction_body.withdrawals.as_ref() else { + return Ok(()); + }; + for (index, (stake_address_bytes, _)) in withdrawals.iter().enumerate() { + let pallas_reward_adddess = + PallasAddress::from_bytes(stake_address_bytes).map_err(|_| { + Box::new(UTxOValidationError::MalformedUTxO { + era: Era::Shelley, + reason: format!("Malformed reward address at withdrawal {index}"), + }) + })?; + + let stake_address = acropolis_codec::map_parameters::map_address(&pallas_reward_adddess) + .map_err(|e| { + Box::new(UTxOValidationError::MalformedUTxO { + era: Era::Shelley, + reason: format!("Invalid reward address at withdrawal {index}: {}", e), + }) + })?; + + let stake_address = match stake_address { + Address::Stake(stake_address) => stake_address, + _ => { + return Err(Box::new(UTxOValidationError::MalformedUTxO { + era: Era::Shelley, + reason: format!("Not a Stake Address at withdrawal {index}"), + })); + } + }; + + if stake_address.network != network_id { + return Err(Box::new(UTxOValidationError::WrongNetworkWithdrawal { + expected: network_id, + wrong_account: stake_address, + withdrawal_index: index, + })); + } + } + + Ok(()) +} + +/// Validate every output has minimum required lovelace +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L531 +pub fn validate_output_too_small_utxo( + transaction_body: &alonzo::TransactionBody, + shelley_params: &ShelleyParams, +) -> UTxOValidationResult { + for (index, output) in transaction_body.outputs.iter().enumerate() { + let lovelace = get_lovelace_from_alonzo_value(&output.amount); + let required_lovelace = compute_min_lovelace(&output.amount, shelley_params); + if lovelace < required_lovelace { + return Err(Box::new(UTxOValidationError::OutputTooSmallUTxO { + output_index: index, + lovelace, + required_lovelace, + })); + } + } + Ok(()) +} + +/// Validate transaction size is under the limit +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L575 +pub fn validate_max_tx_size_utxo( + tx_size: u32, + shelley_params: &ShelleyParams, +) -> UTxOValidationResult { + let max_tx_size = shelley_params.protocol_params.max_tx_size; + if tx_size > max_tx_size { + Err(Box::new(UTxOValidationError::MaxTxSizeUTxO { + supplied: tx_size, + max: max_tx_size, + })) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_utils::TestContext, validation_fixture}; + use pallas::{codec as pallas_codec, ledger::primitives::alonzo::MintedTx as AlonzoMintedTx}; + use test_case::test_case; + + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e") => + matches Ok(()); + )] + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "wrong_ttl") => + matches Err(UTxOValidationError::ExpiredUTxO { ttl: 7084747, current_slot: 7084748 }); + )] + #[allow(clippy::result_large_err)] + fn shelley_test((ctx, raw_tx): (TestContext, Vec)) -> Result<(), UTxOValidationError> { + let alonzo_tx = pallas_codec::minicbor::decode::(&raw_tx).unwrap(); + let mtx = MultiEraTx::from_alonzo_compatible(&alonzo_tx, PallasEra::Shelley); + + let lookup_by_hash = |tx_ref: TxOutRef| -> Result { + ctx.utxos.get(&tx_ref).copied().ok_or_else(|| { + anyhow::anyhow!( + "TxHash not found or already spent: {:?}", + hex::encode(tx_ref.tx_hash) + ) + }) + }; + validate_shelley_tx(&mtx, &ctx.shelley_params, ctx.current_slot, lookup_by_hash) + .map_err(|e| *e) + } +} diff --git a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json index 387fdb9d..b29be2a2 100644 --- a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json +++ b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json @@ -37,5 +37,11 @@ "systemStart": "2017-09-23T21:44:51Z", "updateQuorum": 5, "genDelegs": {} - } + }, + "utxos": [ + [ + ["20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", 0], + [360, 12] + ] + ] } From c985d703d91184007bab9cd2342940a216351a87 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Thu, 27 Nov 2025 18:45:04 +0100 Subject: [PATCH 12/18] fix: cargo shear --- Cargo.lock | 7 ------- modules/tx_validator_phase1/Cargo.toml | 8 -------- 2 files changed, 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74880779..cbcdce1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -491,18 +491,11 @@ dependencies = [ "acropolis_codec", "acropolis_common", "anyhow", - "async-trait", "caryatid_sdk", "config", - "csv", - "hex", "pallas 0.33.0", - "serde", - "serde_json", - "serde_with 3.16.0", "tokio", "tracing", - "tracing-subscriber", ] [[package]] diff --git a/modules/tx_validator_phase1/Cargo.toml b/modules/tx_validator_phase1/Cargo.toml index 57f483cc..4b9b09ba 100644 --- a/modules/tx_validator_phase1/Cargo.toml +++ b/modules/tx_validator_phase1/Cargo.toml @@ -13,19 +13,11 @@ acropolis_common = { path = "../../common" } acropolis_codec = { path = "../../codec" } caryatid_sdk = { workspace = true } - anyhow = { workspace = true } -async-trait = "0.1" config = { workspace = true } -csv = "1" -hex = { workspace = true } pallas = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -serde_with = { workspace = true, features = ["base64"] } tokio = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { version = "0.3.20", features = ["registry", "env-filter"] } [lib] path = "src/tx_validator_phase1.rs" From 62456e0124ff75afe207a1018b5bcdb6901427a9 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 28 Nov 2025 14:15:22 +0100 Subject: [PATCH 13/18] refactor: context for utxo rule test --- .../context.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json index b29be2a2..a468fefc 100644 --- a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json +++ b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json @@ -40,8 +40,8 @@ }, "utxos": [ [ - ["20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", 0], - [360, 12] + ["278ec9a3dfb551288affd8d84b2d6c70b56a153ad1fd43e7b18a5528f687733e", 1], + [4616843, 12] ] ] } From 4f846551e7324357c7525aeaf2a1b05047d7a026 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 28 Nov 2025 14:43:28 +0100 Subject: [PATCH 14/18] refactor: mapping transaction function, and remove unused d tx validation phase1 module --- Cargo.lock | 15 -- Cargo.toml | 1 - codec/src/map_parameters.rs | 88 +++---- common/src/protocol_params.rs | 2 +- common/src/validation.rs | 2 +- modules/chain_store/src/chain_store.rs | 27 ++- modules/stake_delta_filter/src/utils.rs | 4 +- .../NOTES.md => tx_unpacker/VALIDATIONS.md} | 0 modules/tx_unpacker/src/tx_unpacker.rs | 79 +------ modules/tx_validator_phase1/Cargo.toml | 23 -- modules/tx_validator_phase1/src/state.rs | 219 ------------------ .../src/tx_validator_phase1.rs | 164 ------------- processes/omnibus/Cargo.toml | 1 - processes/omnibus/src/main.rs | 2 - 14 files changed, 71 insertions(+), 556 deletions(-) rename modules/{tx_validator_phase1/NOTES.md => tx_unpacker/VALIDATIONS.md} (100%) delete mode 100644 modules/tx_validator_phase1/Cargo.toml delete mode 100644 modules/tx_validator_phase1/src/state.rs delete mode 100644 modules/tx_validator_phase1/src/tx_validator_phase1.rs diff --git a/Cargo.lock b/Cargo.lock index 8309b59d..853dcef0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -494,20 +494,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "acropolis_module_tx_validator_phase1" -version = "0.1.0" -dependencies = [ - "acropolis_codec", - "acropolis_common", - "anyhow", - "caryatid_sdk", - "config", - "pallas 0.33.0", - "tokio", - "tracing", -] - [[package]] name = "acropolis_module_utxo_state" version = "0.1.0" @@ -571,7 +557,6 @@ dependencies = [ "acropolis_module_spo_state", "acropolis_module_stake_delta_filter", "acropolis_module_tx_unpacker", - "acropolis_module_tx_validator_phase1", "acropolis_module_utxo_state", "anyhow", "caryatid_module_clock", diff --git a/Cargo.toml b/Cargo.toml index 691b94a6..2e222c03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,6 @@ members = [ "modules/consensus", # Chooses favoured chain across multiple options "modules/chain_store", # Tracks historical information about blocks and TXs "modules/tx_submitter", # Submits TXs to peers - "modules/tx_validator_phase1", # Validates TXs (simple, without script execution) "modules/block_vrf_validator", # Validate the VRF calculation in the block header "modules/block_kes_validator", # Validate KES in the block header diff --git a/codec/src/map_parameters.rs b/codec/src/map_parameters.rs index d879d385..a4869013 100644 --- a/codec/src/map_parameters.rs +++ b/codec/src/map_parameters.rs @@ -20,7 +20,7 @@ use acropolis_common::{ *, }; use pallas_primitives::conway::PseudoScript; -use pallas_traverse::{MultiEraInput, MultiEraOutput, MultiEraTx}; +use pallas_traverse::{MultiEraInput, MultiEraTx}; use std::{ collections::{HashMap, HashSet}, net::{Ipv4Addr, Ipv6Addr}, @@ -990,66 +990,71 @@ pub fn map_all_governance_voting_procedures( Ok(procs) } -pub struct TransactionRefInfo {} +// pub struct TransactionRefInfo {} -pub fn map_transaction_refs( - inputs: &Vec, - outputs: &Vec<(usize, MultiEraOutput)>, -) -> (Vec, Vec) { - let mut ref_inps = Vec::new(); +// pub fn map_transaction_refs( +// inputs: &Vec, +// outputs: &Vec<(usize, MultiEraOutput)>, +// ) -> (Vec, Vec) { +// let mut ref_inps = Vec::new(); +// for input in inputs { +// // MultiEraInput +// let oref = input.output_ref(); +// let tx_ref = TxOutRef::new(TxHash::from(**oref.hash()), oref.index() as u16); +// ref_inps.push(tx_ref); +// } + +// let mut ref_outs = Vec::new(); +// for (index, output) in outputs {} + +// (ref_inps, ref_outs) +// } + +pub fn map_transaction_inputs(inputs: &Vec) -> Vec { + let mut parsed_inputs = Vec::new(); for input in inputs { // MultiEraInput let oref = input.output_ref(); let tx_ref = TxOutRef::new(TxHash::from(**oref.hash()), oref.index() as u16); - ref_inps.push(tx_ref); - } - let mut ref_outs = Vec::new(); - for (index, output) in outputs {} + parsed_inputs.push(tx_ref); + } - (ref_inps, ref_outs) + parsed_inputs } -#[allow(clippy::type_complexity)] -pub fn map_one_transaction( +pub fn map_transaction_inputs_outputs( block_number: u32, tx_index: u16, tx: &MultiEraTx, ) -> ( - Vec, // inputs - Vec<(TxOutRef, TxOutput)>, // outputs - u128, // total - Vec, // errors + Vec, + Vec<(TxOutRef, TxOutput)>, + Vec, ) { + let mut parsed_inputs = Vec::new(); + let mut parsed_outputs = Vec::new(); + let mut errors = Vec::new(); + let Ok(tx_hash) = tx.hash().to_vec().try_into() else { - return ( - vec![], - vec![], - 0, - vec![ValidationError::MalformedTransaction( - tx_index, - format!("Tx has incorrect hash length ({:?})", tx.hash().to_vec()), - )], - ); + errors.push(ValidationError::MalformedTransaction( + tx_index, + format!("Tx has incorrect hash length ({:?})", tx.hash().to_vec()), + )); + return (parsed_inputs, parsed_outputs, errors); }; let inputs = tx.consumes(); let outputs = tx.produces(); - let mut tx_input_refs = Vec::new(); - let mut tx_outputs = Vec::new(); - let mut total_output = 0; - let mut errors = Vec::new(); - for input in inputs { - // MultiEraInput - let oref = input.output_ref(); - let tx_ref = TxOutRef::new(TxHash::from(**oref.hash()), oref.index() as u16); - - tx_input_refs.push(tx_ref); + let tx_ref = TxOutRef::new( + TxHash::from(**input.output_ref().hash()), + input.output_ref().index() as u16, + ); + parsed_inputs.push(tx_ref); } - // Add all the outputs for (index, output) in outputs { let tx_out_ref = TxOutRef { tx_hash, @@ -1062,7 +1067,7 @@ pub fn map_one_transaction( Ok(pallas_address) => match map_address(&pallas_address) { Ok(address) => { // Add TxOutput to utxo_deltas - tx_outputs.push(( + parsed_outputs.push(( tx_out_ref, TxOutput { utxo_identifier: utxo_id, @@ -1072,9 +1077,6 @@ pub fn map_one_transaction( reference_script: map_reference_script(&output.script_ref()), }, )); - - // catch all output lovelaces - total_output += output.value().coin() as u128; } Err(e) => { errors.push(ValidationError::MalformedTransaction( @@ -1090,7 +1092,7 @@ pub fn map_one_transaction( } } - (tx_input_refs, tx_outputs, total_output, errors) + (parsed_inputs, parsed_outputs, errors) } pub fn map_value(pallas_value: &MultiEraValue) -> Value { diff --git a/common/src/protocol_params.rs b/common/src/protocol_params.rs index dc6b6712..a5194def 100644 --- a/common/src/protocol_params.rs +++ b/common/src/protocol_params.rs @@ -208,7 +208,7 @@ impl From<&ShelleyParams> for PraosParams { epoch_length: params.epoch_length, max_kes_evolutions: params.max_kes_evolutions, max_lovelace_supply: params.max_lovelace_supply, - network_id: params.network_id.clone(), + network_id: params.network_id, slot_length: params.slot_length, slots_per_kes_period: params.slots_per_kes_period, diff --git a/common/src/validation.rs b/common/src/validation.rs index 46f0a3e0..63363add 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -9,7 +9,7 @@ use thiserror::Error; use crate::{ protocol_params::Nonce, Address, Era, GenesisKeyhash, Lovelace, NetworkId, PoolId, Slot, - StakeAddress, TxOutRef, TxOutput, Value, VrfKeyHash, + StakeAddress, TxOutRef, Value, VrfKeyHash, }; /// Transaction Validation Error diff --git a/modules/chain_store/src/chain_store.rs b/modules/chain_store/src/chain_store.rs index d43c46e2..09547d23 100644 --- a/modules/chain_store/src/chain_store.rs +++ b/modules/chain_store/src/chain_store.rs @@ -113,7 +113,6 @@ impl ChainStore { let query_store = store.clone(); context.handle(&txs_queries_topic, move |req| { let query_store = query_store.clone(); - let network_id = network_id.clone(); async move { let Message::StateQuery(StateQuery::Transactions(query)) = req.as_ref() else { return Arc::new(Message::StateQueryResponse( @@ -893,14 +892,14 @@ impl ChainStore { alonzo::Certificate::StakeRegistration(cred) => { certs.push(TransactionStakeCertificate { index: index as u64, - address: map_stake_address(cred, network_id.clone()), + address: map_stake_address(cred, network_id), registration: true, }); } alonzo::Certificate::StakeDeregistration(cred) => { certs.push(TransactionStakeCertificate { index: index as u64, - address: map_stake_address(cred, network_id.clone()), + address: map_stake_address(cred, network_id), registration: false, }); } @@ -910,28 +909,28 @@ impl ChainStore { conway::Certificate::StakeRegistration(cred) => { certs.push(TransactionStakeCertificate { index: index as u64, - address: map_stake_address(cred, network_id.clone()), + address: map_stake_address(cred, network_id), registration: true, }); } conway::Certificate::StakeDeregistration(cred) => { certs.push(TransactionStakeCertificate { index: index as u64, - address: map_stake_address(cred, network_id.clone()), + address: map_stake_address(cred, network_id), registration: false, }); } conway::Certificate::StakeRegDeleg(cred, _, _) => { certs.push(TransactionStakeCertificate { index: index as u64, - address: map_stake_address(cred, network_id.clone()), + address: map_stake_address(cred, network_id), registration: true, }); } conway::Certificate::StakeVoteRegDeleg(cred, _, _, _) => { certs.push(TransactionStakeCertificate { index: index as u64, - address: map_stake_address(cred, network_id.clone()), + address: map_stake_address(cred, network_id), registration: true, }); } @@ -961,7 +960,7 @@ impl ChainStore { { certs.push(TransactionDelegationCertificate { index: index as u64, - address: map_stake_address(cred, network_id.clone()), + address: map_stake_address(cred, network_id), pool_id: to_pool_id(pool_key_hash), active_epoch: tx.block.extra.epoch + 1, }); @@ -971,7 +970,7 @@ impl ChainStore { conway::Certificate::StakeDelegation(cred, pool_key_hash) => { certs.push(TransactionDelegationCertificate { index: index as u64, - address: map_stake_address(cred, network_id.clone()), + address: map_stake_address(cred, network_id), pool_id: to_pool_id(pool_key_hash), active_epoch: tx.block.extra.epoch + 1, }); @@ -979,7 +978,7 @@ impl ChainStore { conway::Certificate::StakeRegDeleg(cred, pool_key_hash, _) => { certs.push(TransactionDelegationCertificate { index: index as u64, - address: map_stake_address(cred, network_id.clone()), + address: map_stake_address(cred, network_id), pool_id: to_pool_id(pool_key_hash), active_epoch: tx.block.extra.epoch + 1, }); @@ -987,7 +986,7 @@ impl ChainStore { conway::Certificate::StakeVoteRegDeleg(cred, pool_key_hash, _, _) => { certs.push(TransactionDelegationCertificate { index: index as u64, - address: map_stake_address(cred, network_id.clone()), + address: map_stake_address(cred, network_id), pool_id: to_pool_id(pool_key_hash), active_epoch: tx.block.extra.epoch + 1, }); @@ -1041,7 +1040,7 @@ impl ChainStore { InstantaneousRewardSource::Treasury } }, - address: map_stake_address(&cred, network_id.clone()), + address: map_stake_address(&cred, network_id), amount: amount as u64, }); } @@ -1093,7 +1092,7 @@ impl ChainStore { pool_owners, relays, pool_metadata, - network_id.clone(), + network_id, false, )?, // Pool registration/updates become active after 2 epochs @@ -1126,7 +1125,7 @@ impl ChainStore { pool_owners, relays, pool_metadata, - network_id.clone(), + network_id, false, )?, // Pool registration/updates become active after 2 epochs diff --git a/modules/stake_delta_filter/src/utils.rs b/modules/stake_delta_filter/src/utils.rs index c839a8b7..9bebc7c6 100644 --- a/modules/stake_delta_filter/src/utils.rs +++ b/modules/stake_delta_filter/src/utils.rs @@ -354,12 +354,12 @@ pub fn process_message( let stake_address = match &shelley.delegation { // Base addresses (stake delegated to itself) ShelleyAddressDelegationPart::StakeKeyHash(keyhash) => StakeAddress { - network: shelley.network.clone(), + network: shelley.network, credential: StakeCredential::AddrKeyHash(*keyhash), }, ShelleyAddressDelegationPart::ScriptHash(scripthash) => StakeAddress { - network: shelley.network.clone(), + network: shelley.network, credential: StakeCredential::ScriptHash(*scripthash), }, diff --git a/modules/tx_validator_phase1/NOTES.md b/modules/tx_unpacker/VALIDATIONS.md similarity index 100% rename from modules/tx_validator_phase1/NOTES.md rename to modules/tx_unpacker/VALIDATIONS.md diff --git a/modules/tx_unpacker/src/tx_unpacker.rs b/modules/tx_unpacker/src/tx_unpacker.rs index 797ee281..4babbfc0 100644 --- a/modules/tx_unpacker/src/tx_unpacker.rs +++ b/modules/tx_unpacker/src/tx_unpacker.rs @@ -173,24 +173,21 @@ impl TxUnpacker { let tx_hash: TxHash = tx.hash().to_vec().try_into().expect("invalid tx hash length"); let tx_identifier = TxIdentifier::new(block_number, tx_index); - let inputs = tx.consumes(); - let outputs = tx.produces(); let certs = tx.certs(); let tx_withdrawals = tx.withdrawals_sorted_set(); let mut props = None; let mut votes = None; - let (txs_ref_in, tx_out, total, errors) = - map_parameters::map_one_transaction( + let (tx_inputs, tx_outputs, errors) = + map_parameters::map_transaction_inputs_outputs( block_number, tx_index, &tx ); if tracing::enabled!(tracing::Level::DEBUG) { debug!( "Decoded tx with {} inputs, {} outputs, {} certs, {} errors", - //inputs.len(), outputs.len(), certs.len()); - txs_ref_in.len(), - tx_out.len(), + tx_inputs.len(), + tx_outputs.len(), certs.len(), errors.len() ) @@ -200,30 +197,9 @@ impl TxUnpacker { // Lookup and remove UTxOIdentifier from registry // Group deltas by tx let mut tx_utxo_deltas = TxUTxODeltas {tx_identifier, inputs: Vec::new(), outputs: Vec::new()}; -/* - // Remove inputs from UTxORegistry and push to UTxOIdentifiers to delta - for input in inputs { - let oref = input.output_ref(); - let tx_ref = TxOutRef::new(TxHash::from(**oref.hash()), oref.index() as u16); - - match utxo_registry.consume(&tx_ref) { - Ok(tx_identifier) => { - tx_utxo_deltas.inputs.push( - UTxOIdentifier::new( - tx_identifier.block_number(), - tx_identifier.tx_index(), - tx_ref.output_index, - ), - ); - } - Err(e) => { - error!("Failed to consume input {}: {e}", tx_ref.output_index); - } - } - } - */ - for tx_ref in txs_ref_in { + // Remove inputs from UTxORegistry and push to UTxOIdentifiers to delta + for tx_ref in tx_inputs { match utxo_registry.consume(&tx_ref) { Ok(tx_identifier) => { // Add TxInput to utxo_deltas @@ -241,45 +217,9 @@ impl TxUnpacker { } } - // Add outputs to UTxORegistry and push TxOutputs to delta - /* - for (index, output) in outputs { - match utxo_registry.add( - block_number, - tx_index, - TxOutRef { - tx_hash, - output_index: index as u16, - }, - ) { - Ok(utxo_id) => { - match output.address() { - Ok(pallas_address) => match map_parameters::map_address(&pallas_address) { - Ok(address) => { - tx_utxo_deltas.outputs.push(TxOutput { - utxo_identifier: utxo_id, - address, - value: map_parameters::map_value(&output.value()), - datum: map_parameters::map_datum(&output.datum()), - reference_script: map_parameters::map_reference_script(&output.script_ref()) - }); - - // catch all output lovelaces - total_output += output.value().coin() as u128; - } - Err(e) => error!("Output {index} in tx ignored: {e}"), - }, - Err(e) => error!("Can't parse output {index} in tx: {e}"), - } - } - Err(e) => { - error!("Failed to insert output into registry: {e}"); - } - } - } - */ + for (tx_ref, output) in tx_outputs.into_iter() { + total_output += output.value.coin() as u128; - for (tx_ref, output) in tx_out.into_iter() { if let Err(e) = utxo_registry.add( block_number, tx_index, @@ -292,7 +232,6 @@ impl TxUnpacker { } } - total_output += total; utxo_deltas.push(tx_utxo_deltas); } @@ -325,7 +264,7 @@ impl TxUnpacker { if publish_certificates_topic.is_some() { for ( cert_index, cert) in certs.iter().enumerate() { - match map_parameters::map_certificate(cert, tx_identifier, cert_index, network_id.clone()) { + match map_parameters::map_certificate(cert, tx_identifier, cert_index, network_id) { Ok(tx_cert) => { certificates.push(tx_cert); }, diff --git a/modules/tx_validator_phase1/Cargo.toml b/modules/tx_validator_phase1/Cargo.toml deleted file mode 100644 index 4b9b09ba..00000000 --- a/modules/tx_validator_phase1/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -# Acropolis Governance state module - -[package] -name = "acropolis_module_tx_validator_phase1" -version = "0.1.0" -edition = "2021" -authors = ["Dmitry Shtukenberg "] -description = "Transaction validator, phase 1" -license = "Apache-2.0" - -[dependencies] -acropolis_common = { path = "../../common" } -acropolis_codec = { path = "../../codec" } - -caryatid_sdk = { workspace = true } -anyhow = { workspace = true } -config = { workspace = true } -pallas = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } - -[lib] -path = "src/tx_validator_phase1.rs" diff --git a/modules/tx_validator_phase1/src/state.rs b/modules/tx_validator_phase1/src/state.rs deleted file mode 100644 index 1cd58943..00000000 --- a/modules/tx_validator_phase1/src/state.rs +++ /dev/null @@ -1,219 +0,0 @@ -use crate::TxValidatorPhase1StateConfig; -use acropolis_codec::map_parameters; -use acropolis_common::messages::{ProtocolParamsMessage, RawTxsMessage}; -use acropolis_common::validation::{ValidationError, ValidationStatus}; -use acropolis_common::{ - AssetName, BlockInfo, NativeAsset, NativeAssets, TxHash, TxIdentifier, TxOutRef, TxOutput, - UTxOIdentifier, Value, -}; -use anyhow::Result; -use pallas::ledger::primitives::{alonzo, byron}; -use pallas::ledger::traverse::{MultiEraPolicyAssets, MultiEraTx, MultiEraValue}; -use std::borrow::Cow; -use std::collections::HashMap; -use std::sync::Arc; -use tracing::error; - -// TODO: make something with separate utxo registres -#[derive(Clone, Default)] -pub struct UTxORegistry { - pub live_map: HashMap, -} - -pub struct State { - pub config: Arc, - params: Option, - utxos_registry: UTxORegistry, -} - -enum ConversionResult { - Ok(Res), - Error(ValidationError), -} - -/* -pub fn map_value(pallas_value: &MultiEraValue) -> Value { - let lovelace = pallas_value.coin(); - let pallas_assets = pallas_value.assets(); - - let mut assets: NativeAssets = Vec::new(); - - for policy_group in pallas_assets { - match policy_group { - MultiEraPolicyAssets::AlonzoCompatibleOutput(policy, kvps) => { - match policy.as_ref().try_into() { - Ok(policy_id) => { - let native_assets = kvps - .iter() - .filter_map(|(name, amt)| { - AssetName::new(name).map(|asset_name| NativeAsset { - name: asset_name, - amount: *amt, - }) - }) - .collect::>(); - - assets.push((policy_id, native_assets)); - } - Err(_) => { - tracing::error!( - "Invalid policy id length: expected 28 bytes, got {}", - policy.len() - ); - continue; - } - } - } - MultiEraPolicyAssets::ConwayOutput(policy, kvps) => match policy.as_ref().try_into() { - Ok(policy_id) => { - let native_assets = kvps - .iter() - .filter_map(|(name, amt)| { - AssetName::new(name).map(|asset_name| NativeAsset { - name: asset_name, - amount: u64::from(*amt), - }) - }) - .collect(); - - assets.push((policy_id, native_assets)); - } - Err(_) => { - tracing::error!( - "Invalid policy id length: expected 28 bytes, got {}", - policy.len() - ); - continue; - } - }, - _ => {} - } - } - Value::new(lovelace, assets) -} - */ - -struct Transaction { - inputs: Vec, - outputs: Vec, -} - -impl State { - pub fn new(config: Arc) -> Self { - Self { - config, - params: None, - utxos_registry: UTxORegistry::default(), - } - } - - pub async fn process_params( - &mut self, - _blk: BlockInfo, - prm: ProtocolParamsMessage, - ) -> Result<()> { - self.params = Some(prm); - Ok(()) - } - - /// Byron validation is not acutally performed, so it's always returns 'Go' - fn validate_byron<'b>( - &self, - _tx: Box>>, - ) -> Result { - Ok(ValidationStatus::Go) - } - - fn convert_from_pallas_tx<'b>( - &self, - block_info: &BlockInfo, - tx_index: u16, - tx: &MultiEraTx, - ) -> Result> { - let _certs = tx.certs(); - let _tx_withdrawals = tx.withdrawals_sorted_set(); - - let (tx_in_ref, tx_out, _total, err) = - map_parameters::map_one_transaction(block_info.number as u32, tx_index, tx); - - if let Some(first_err) = err.into_iter().next() { - return Ok(ConversionResult::Error(first_err)); - } - - let mut converted_inputs = Vec::new(); - let mut converted_outputs = Vec::new(); - - for tx_ref in tx_in_ref { - // MultiEraInput - // Lookup and remove UTxOIdentifier from registry - match self.utxos_registry.live_map.get(&tx_ref) { - Some(tx_identifier) => { - // Add TxInput to utxo_deltas - converted_inputs.push(UTxOIdentifier::new( - tx_identifier.block_number(), - tx_identifier.tx_index(), - tx_ref.output_index, - )); - } - None => { - return Ok(ConversionResult::Error( - ValidationError::MalformedTransaction( - tx_index, - format!( - "Tx not found, tx {}, output index {}", - tx_ref.tx_hash, tx_ref.output_index - ), - ), - )); - } - } - } - - // Add all the outputs - for (_tx_ref, output) in tx_out { - converted_outputs.push(output); - } - - let tx = Transaction { - inputs: converted_inputs, - outputs: converted_outputs, - }; - - Ok(ConversionResult::Ok(tx)) - } - - fn validate_tx(&self, tx: &Transaction) -> Result { - // Do validate transactions - Ok(ValidationStatus::Go) - } - - pub fn process_transactions( - &mut self, - blk: &BlockInfo, - txs_msg: &RawTxsMessage, - ) -> Result { - for (tx_index, raw_tx) in txs_msg.txs.iter().enumerate() { - // Parse the tx - let res = match MultiEraTx::decode(raw_tx) { - Err(e) => ValidationStatus::NoGo(ValidationError::CborDecodeError( - tx_index, - e.to_string(), - )), - Ok(MultiEraTx::Byron(byron_tx)) => self.validate_byron(byron_tx)?, - - Ok(tx) => { - let tx = match self.convert_from_pallas_tx(blk, tx_index as u16, &tx)? { - ConversionResult::Ok(res) => res, - ConversionResult::Error(err) => return Ok(ValidationStatus::NoGo(err)), - }; - self.validate_tx(&tx)? - } - }; - - if let ValidationStatus::NoGo(_) = &res { - return Ok(res); - } - } - Ok(ValidationStatus::Go) - } -} diff --git a/modules/tx_validator_phase1/src/tx_validator_phase1.rs b/modules/tx_validator_phase1/src/tx_validator_phase1.rs deleted file mode 100644 index 9d084009..00000000 --- a/modules/tx_validator_phase1/src/tx_validator_phase1.rs +++ /dev/null @@ -1,164 +0,0 @@ -//! Acropolis transaction unpacker module for Caryatid -//! Unpacks transaction bodies into UTXO events - -mod state; - -use acropolis_common::{ - messages::{CardanoMessage, Message, ProtocolParamsMessage, RawTxsMessage}, - *, -}; - -use acropolis_codec::map_parameters; - -use caryatid_sdk::{module, Context, Module, Subscription}; -use std::{clone::Clone, sync::Arc}; - -use crate::state::State; -use acropolis_common::validation::ValidationStatus; -use anyhow::{anyhow, bail, Result}; -use config::Config; -use tracing::{error, info}; -//mod utxo_registry; -//use crate::utxo_registry::UTxORegistry; - -const DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC: (&str, &str) = - ("transactions-subscribe-topic", "cardano.txs"); -const DEFAULT_PROTOCOL_PARAMETERS_TOPIC: (&str, &str) = - ("parameters-topic", "cardano.protocol.parameters"); -const DEFAULT_VALIDATION_RESULT_TOPIC: (&str, &str) = ( - "publish-valiadtion-result-topic", - "cardano.validation.tx-phase-1", -); -const DEFAULT_NETWORK_NAME: (&str, &str) = ("network-name", "mainnet"); - -//const CIP25_METADATA_LABEL: u64 = 721; - -/// Tx unpacker module -/// Parameterised by the outer message enum used on the bus -#[module( - message_type(Message), - name = "tx-validator-phase1", - description = "Transactions validator, Phase 1" -)] -pub struct TxValidatorPhase1; - -struct TxValidatorPhase1StateConfig { - pub context: Arc>, - pub transactions_subscribe_topic: String, - pub genesis_utxos_subscribe_topic: String, - pub publish_validation_result: String, - pub params_subscribe_topic: String, - #[allow(dead_code)] - pub network_name: String, -} - -impl TxValidatorPhase1StateConfig { - fn conf(config: &Arc, keydef: (&str, &str)) -> String { - let actual = config.get_string(keydef.0).unwrap_or(keydef.1.to_string()); - info!("Parameter value '{}' for {}", actual, keydef.0); - actual - } - - pub fn new(context: &Arc>, config: &Arc) -> Arc { - Arc::new(Self { - context: context.clone(), - transactions_subscribe_topic: Self::conf(config, DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC), - genesis_utxos_subscribe_topic: Self::conf(config, DEFAULT_PROTOCOL_PARAMETERS_TOPIC), - params_subscribe_topic: Self::conf(config, DEFAULT_PROTOCOL_PARAMETERS_TOPIC), - publish_validation_result: Self::conf(config, DEFAULT_VALIDATION_RESULT_TOPIC), - network_name: Self::conf(config, DEFAULT_NETWORK_NAME), - }) - } -} - -impl TxValidatorPhase1 { - async fn read_parameters( - parameters_s: &mut Box>, - ) -> Result<(BlockInfo, ProtocolParamsMessage)> { - match parameters_s.read().await?.1.as_ref() { - Message::Cardano((blk, CardanoMessage::ProtocolParams(params))) => { - Ok((blk.clone(), params.clone())) - } - msg => Err(anyhow!( - "Unexpected message {msg:?} for protocol parameters topic" - )), - } - } - - async fn read_transactions( - transaction_s: &mut Box>, - ) -> Result<(BlockInfo, RawTxsMessage)> { - match transaction_s.read().await?.1.as_ref() { - Message::Cardano((blk, CardanoMessage::ReceivedTxs(tx))) => { - Ok((blk.clone(), tx.clone())) - } - msg => Err(anyhow!("Unexpected message {msg:?} for transaction topic")), - } - } - - async fn publish_result( - config: &TxValidatorPhase1StateConfig, - block: BlockInfo, - result: ValidationStatus, - ) -> Result<()> { - if let ValidationStatus::NoGo(res) = &result { - error!("Cannot validate transaction: {:?}", res); - } - - let packed_message = Arc::new(Message::Cardano(( - block.clone(), - CardanoMessage::BlockValidation(result), - ))); - let context = config.context.clone(); - let topic = config.publish_validation_result.clone(); - - tokio::spawn(async move { - context - .publish(&topic, packed_message) - .await - .unwrap_or_else(|e| tracing::error!("Failed to publish: {e}")); - }); - - Ok(()) - } - - async fn run( - state: &mut State, - mut _gen: Box>, - mut txs: Box>, - mut params: Box>, - ) -> Result<()> { - loop { - let (trx_b, trx) = Self::read_transactions(&mut txs).await?; - if trx_b.new_epoch { - let (prm_b, prm) = Self::read_parameters(&mut params).await?; - if prm_b != trx_b { - bail!("Blocks are out of sync: transaction {trx_b:?} != params {prm_b:?}"); - } - state.process_params(prm_b, prm).await?; - } - let response = state.process_transactions(&trx_b, &trx)?; - Self::publish_result(&state.config, trx_b, response).await?; - } - } - - /// Main init function - pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { - // Get configuration - let config = TxValidatorPhase1StateConfig::new(&context, &config); - - // Subscribe to genesis and txs topics - let gen_sub = context.subscribe(&config.genesis_utxos_subscribe_topic).await?; - let txs_sub = context.subscribe(&config.transactions_subscribe_topic).await?; - let params_sub = context.subscribe(&config.params_subscribe_topic).await?; - - context.clone().run(async move { - let mut state = State::new(config.clone()); - TxValidatorPhase1::run(&mut state, gen_sub, txs_sub, params_sub) - .await - .unwrap_or_else(|e| error!("TX validator failed: {e}")); - }); - - Ok(()) - } -} diff --git a/processes/omnibus/Cargo.toml b/processes/omnibus/Cargo.toml index 2ff93890..bb171711 100644 --- a/processes/omnibus/Cargo.toml +++ b/processes/omnibus/Cargo.toml @@ -31,7 +31,6 @@ acropolis_module_address_state = { path = "../../modules/address_state" } acropolis_module_consensus = { path = "../../modules/consensus" } acropolis_module_historical_accounts_state = { path = "../../modules/historical_accounts_state" } acropolis_module_historical_epochs_state = { path = "../../modules/historical_epochs_state" } -acropolis_module_tx_validator_phase1 = { path = "../../modules/tx_validator_phase1" } acropolis_module_block_vrf_validator = { path = "../../modules/block_vrf_validator" } acropolis_module_block_kes_validator = { path = "../../modules/block_kes_validator" } acropolis_module_snapshot_bootstrapper = { path = "../../modules/snapshot_bootstrapper" } diff --git a/processes/omnibus/src/main.rs b/processes/omnibus/src/main.rs index 01bbdad1..49f2cea3 100644 --- a/processes/omnibus/src/main.rs +++ b/processes/omnibus/src/main.rs @@ -32,7 +32,6 @@ use acropolis_module_spdd_state::SPDDState; use acropolis_module_spo_state::SPOState; use acropolis_module_stake_delta_filter::StakeDeltaFilter; use acropolis_module_tx_unpacker::TxUnpacker; -use acropolis_module_tx_validator_phase1::TxValidatorPhase1; use acropolis_module_utxo_state::UTXOState; use caryatid_module_clock::Clock; @@ -152,7 +151,6 @@ pub async fn main() -> Result<()> { DRDDState::register(&mut process); Consensus::register(&mut process); ChainStore::register(&mut process); - TxValidatorPhase1::register(&mut process); BlockVrfValidator::register(&mut process); BlockKesValidator::register(&mut process); From 109d632f1b96b2c6e3f43eb59b44469ab800edad Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 28 Nov 2025 15:07:42 +0100 Subject: [PATCH 15/18] fix: don't check byron address's network --- modules/tx_unpacker/src/validations/shelley/utxo.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/modules/tx_unpacker/src/validations/shelley/utxo.rs b/modules/tx_unpacker/src/validations/shelley/utxo.rs index 81545440..7df6e74f 100644 --- a/modules/tx_unpacker/src/validations/shelley/utxo.rs +++ b/modules/tx_unpacker/src/validations/shelley/utxo.rs @@ -188,8 +188,11 @@ pub fn validate_wrong_network( }) })?; - let address = match address { - Address::Shelley(shelley_address) => shelley_address, + let is_network_correct = match &address { + // NOTE: + // need to parse byron address's attributes and get network magic + Address::Byron(_) => true, + Address::Shelley(shelley_address) => shelley_address.network == network_id, _ => { return Err(Box::new(UTxOValidationError::MalformedUTxO { era: Era::Shelley, @@ -197,10 +200,10 @@ pub fn validate_wrong_network( })) } }; - if address.network != network_id { + if !is_network_correct { return Err(Box::new(UTxOValidationError::WrongNetwork { expected: network_id, - wrong_address: Address::Shelley(address), + wrong_address: address, output_index: index, })); } From 20f33e897f20fb3638480005e3ce694661f7dffc Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 28 Nov 2025 16:19:46 +0100 Subject: [PATCH 16/18] feat: add state to tx unpacker for tx validation --- Cargo.lock | 1 + modules/tx_unpacker/Cargo.toml | 2 +- modules/tx_unpacker/src/state.rs | 45 + modules/tx_unpacker/src/tx_unpacker.rs | 807 ++++++++++-------- modules/tx_unpacker/src/validations/mod.rs | 1 + .../src/validations/shelley/mod.rs | 20 + .../src/validations/shelley/utxo.rs | 14 - processes/omnibus/omnibus.toml | 2 - 8 files changed, 540 insertions(+), 352 deletions(-) create mode 100644 modules/tx_unpacker/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 853dcef0..c73a1cc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -491,6 +491,7 @@ dependencies = [ "serde", "serde_json", "test-case", + "tokio", "tracing", ] diff --git a/modules/tx_unpacker/Cargo.toml b/modules/tx_unpacker/Cargo.toml index 5e890efb..ce3bbc0e 100644 --- a/modules/tx_unpacker/Cargo.toml +++ b/modules/tx_unpacker/Cargo.toml @@ -13,11 +13,11 @@ acropolis_common = { path = "../../common" } acropolis_codec = { path = "../../codec" } caryatid_sdk = { workspace = true } - anyhow = { workspace = true } config = { workspace = true } futures = "0.3.31" hex = { workspace = true } +tokio = { workspace = true } pallas = { workspace = true } tracing = { workspace = true } serde = { workspace = true } diff --git a/modules/tx_unpacker/src/state.rs b/modules/tx_unpacker/src/state.rs new file mode 100644 index 00000000..05a00b1e --- /dev/null +++ b/modules/tx_unpacker/src/state.rs @@ -0,0 +1,45 @@ +use crate::{utxo_registry::UTxORegistry, validations}; +use acropolis_common::{ + messages::ProtocolParamsMessage, protocol_params::ProtocolParams, + validation::TransactionValidationError, BlockInfo, Era, +}; +use anyhow::Result; +use pallas::ledger::traverse::MultiEraTx; + +#[derive(Default, Clone)] +pub struct State { + pub protocol_params: ProtocolParams, +} + +impl State { + pub fn new() -> Self { + Self { + protocol_params: ProtocolParams::default(), + } + } + + pub fn handle_protocol_params(&mut self, msg: &ProtocolParamsMessage) { + self.protocol_params = msg.params.clone(); + } + + pub fn validate_transaction( + &self, + block_info: &BlockInfo, + tx: &MultiEraTx, + utxo_registry: &UTxORegistry, + ) -> Result<(), TransactionValidationError> { + match block_info.era { + Era::Shelley => { + let Some(shelley_params) = self.protocol_params.shelley.as_ref() else { + return Err(TransactionValidationError::Other( + "Shelley params are not set".to_string(), + )); + }; + validations::validate_shelley_tx(tx, shelley_params, block_info.slot, |tx_ref| { + utxo_registry.lookup_by_hash(tx_ref) + }) + } + _ => Ok(()), + } + } +} diff --git a/modules/tx_unpacker/src/tx_unpacker.rs b/modules/tx_unpacker/src/tx_unpacker.rs index 4babbfc0..9d239982 100644 --- a/modules/tx_unpacker/src/tx_unpacker.rs +++ b/modules/tx_unpacker/src/tx_unpacker.rs @@ -7,24 +7,28 @@ use acropolis_common::{ AssetDeltasMessage, BlockTxsMessage, CardanoMessage, GovernanceProceduresMessage, Message, TxCertificatesMessage, UTXODeltasMessage, WithdrawalsMessage, }, + state_history::{StateHistory, StateHistoryStore}, *, }; use anyhow::Result; -use caryatid_sdk::{module, Context}; +use caryatid_sdk::{module, Context, Subscription}; use config::Config; use futures::future::join_all; use pallas::codec::minicbor::encode; use pallas::ledger::primitives::KeyValuePairs; use pallas::ledger::{primitives, traverse, traverse::MultiEraTx}; use std::{clone::Clone, fmt::Debug, sync::Arc}; -use tracing::{debug, error, info, info_span, Instrument}; +use tokio::sync::Mutex; +use tracing::{debug, error, info, info_span}; +mod state; mod utxo_registry; mod validations; -use crate::utxo_registry::UTxORegistry; +use crate::{state::State, utxo_registry::UTxORegistry}; mod test_utils; const DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC: &str = "cardano.txs"; const DEFAULT_GENESIS_SUBSCRIBE_TOPIC: &str = "cardano.genesis.utxos"; +const DEFAULT_PROTOCOL_PARAMS_SUBSCRIBE_TOPIC: &str = "cardano.protocol.parameters"; const CIP25_METADATA_LABEL: u64 = 721; @@ -38,426 +42,559 @@ const CIP25_METADATA_LABEL: u64 = 721; pub struct TxUnpacker; impl TxUnpacker { - fn decode_updates( - dest: &mut Vec, - proposals: &KeyValuePairs, - epoch: u64, - map: impl Fn(&EraSpecificUpdateProposals) -> Result>, - ) { - let mut update = AlonzoBabbageUpdateProposal { - proposals: Vec::new(), - enactment_epoch: epoch, - }; - - for (hash_bytes, vote) in proposals.iter() { - let hash = match GenesisKeyhash::try_from(hash_bytes.as_ref()) { - Ok(h) => h, - Err(e) => { - error!("Invalid genesis keyhash in protocol parameter update: {e}"); - continue; - } - }; - - match map(vote) { - Ok(upd) => update.proposals.push((hash, upd)), - Err(e) => error!("Cannot convert protocol param update {vote:?}: {e}"), + #[allow(clippy::too_many_arguments)] + async fn run( + context: Arc>, + network_id: NetworkId, + history: Arc>>, + mut utxo_registry: UTxORegistry, + // publishers + publish_utxo_deltas_topic: Option, + publish_asset_deltas_topic: Option, + publish_withdrawals_topic: Option, + publish_certificates_topic: Option, + publish_governance_procedures_topic: Option, + publish_block_txs_topic: Option, + // subscribers + mut genesis_sub: Box>, + mut txs_sub: Box>, + mut protocol_params_sub: Box>, + ) -> Result<()> { + // Initialize TxRegistry with genesis utxos + let (_, message) = genesis_sub.read().await.expect("failed to read genesis utxos"); + match message.as_ref() { + Message::Cardano((_block, CardanoMessage::GenesisUTxOs(genesis_msg))) => { + utxo_registry.bootstrap_from_genesis_utxos(&genesis_msg.utxos); + info!( + "Seeded registry with {} genesis utxos", + genesis_msg.utxos.len() + ); } + other => panic!("expected GenesisUTxOs, got {:?}", other), } - dest.push(update); - } - /// Main init function - pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { - // Get configuration - let transactions_subscribe_topic = config - .get_string("subscribe-topic") - .unwrap_or(DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC.to_string()); - info!("Creating subscriber on '{transactions_subscribe_topic}'"); - - let genesis_utxos_subscribe_topic = config - .get_string("genesis-utxos-subscribe-topic") - .unwrap_or(DEFAULT_GENESIS_SUBSCRIBE_TOPIC.to_string()); - info!("Creating subscriber on '{genesis_utxos_subscribe_topic}'"); - - let publish_utxo_deltas_topic = config.get_string("publish-utxo-deltas-topic").ok(); - if let Some(ref topic) = publish_utxo_deltas_topic { - info!("Publishing UTXO deltas on '{topic}'"); - } - - let publish_asset_deltas_topic = config.get_string("publish-asset-deltas-topic").ok(); - if let Some(ref topic) = publish_asset_deltas_topic { - info!("Publishing native asset deltas on '{topic}'"); - } - - let publish_withdrawals_topic = config.get_string("publish-withdrawals-topic").ok(); - if let Some(ref topic) = publish_withdrawals_topic { - info!("Publishing withdrawals on '{topic}'"); - } - - let publish_certificates_topic = config.get_string("publish-certificates-topic").ok(); - if let Some(ref topic) = publish_certificates_topic { - info!("Publishing certificates on '{topic}'"); - } - - let publish_governance_procedures_topic = - config.get_string("publish-governance-topic").ok(); - if let Some(ref topic) = publish_governance_procedures_topic { - info!("Publishing governance procedures on '{topic}'"); - } - - let publish_block_txs_topic = config.get_string("publish-block-txs-topic").ok(); - if let Some(ref topic) = publish_block_txs_topic { - info!("Publishing block txs on '{topic}'"); - } - - let network_id: NetworkId = - config.get_string("network-id").unwrap_or("mainnet".to_string()).into(); - - // Initialize UTxORegistry - let mut utxo_registry = UTxORegistry::default(); + loop { + let mut state = history.lock().await.get_or_init_with(State::new); + let mut current_block: Option = None; - // Subscribe to genesis and txs topics - let mut genesis_sub = context.subscribe(&genesis_utxos_subscribe_topic).await?; - let mut txs_sub = context.subscribe(&transactions_subscribe_topic).await?; + let Ok((_, message)) = txs_sub.read().await else { + return Err(anyhow::anyhow!("failed to read txs")); + }; + let new_epoch = match message.as_ref() { + Message::Cardano((block_info, _)) => { + // Handle rollbacks on this topic only + if block_info.status == BlockStatus::RolledBack { + state = history.lock().await.get_rolled_back_state(block_info.number); + } + current_block = Some(block_info.clone()); - context.clone().run(async move { - // Initialize TxRegistry with genesis utxos - let (_, message) = genesis_sub.read().await - .expect("failed to read genesis utxos"); - match message.as_ref() { - Message::Cardano((_block, CardanoMessage::GenesisUTxOs(genesis_msg))) => { - utxo_registry.bootstrap_from_genesis_utxos(&genesis_msg.utxos); - info!("Seeded registry with {} genesis utxos", genesis_msg.utxos.len()); + // new_epoch? + block_info.new_epoch } - other => panic!("expected GenesisUTxOs, got {:?}", other), - } - loop { - let Ok((_, message)) = txs_sub.read().await else { return; }; - match message.as_ref() { - Message::Cardano((block, CardanoMessage::ReceivedTxs(txs_msg))) => { - let span = info_span!("tx_unpacker.run", block = block.number); - - async { - if tracing::enabled!(tracing::Level::DEBUG) { - debug!("Received {} txs for slot {}", - txs_msg.txs.len(), block.slot); - } - - let mut utxo_deltas = Vec::new(); - let mut asset_deltas = Vec::new(); - let mut cip25_metadata_updates = Vec::new(); - let mut withdrawals = Vec::new(); - let mut certificates = Vec::new(); - let mut voting_procedures = Vec::new(); - let mut proposal_procedures = Vec::new(); - let mut alonzo_babbage_update_proposals = Vec::new(); - let mut total_output: u128 = 0; - let mut total_fees: u64 = 0; - let total_txs = txs_msg.txs.len() as u64; - - // handle rollback or advance registry to the next block - let block_number = block.number as u32; - if block.status == BlockStatus::RolledBack { - if let Err(e) = utxo_registry.rollback_before(block_number) { - error!("rollback_before({}) failed: {}", block_number, e); - } - utxo_registry.next_block(); - } - for (tx_index , raw_tx) in txs_msg.txs.iter().enumerate() { - let tx_index = tx_index as u16; - - // Parse the tx - match MultiEraTx::decode(raw_tx) { - Ok(tx) => { - let tx_hash: TxHash = tx.hash().to_vec().try_into().expect("invalid tx hash length"); - let tx_identifier = TxIdentifier::new(block_number, tx_index); + _ => { + error!("Unexpected message type: {message:?}"); + false + } + }; - let certs = tx.certs(); - let tx_withdrawals = tx.withdrawals_sorted_set(); - let mut props = None; - let mut votes = None; + match message.as_ref() { + Message::Cardano((block, CardanoMessage::ReceivedTxs(txs_msg))) => { + if tracing::enabled!(tracing::Level::DEBUG) { + debug!("Received {} txs for slot {}", txs_msg.txs.len(), block.slot); + } - let (tx_inputs, tx_outputs, errors) = - map_parameters::map_transaction_inputs_outputs( - block_number, tx_index, &tx - ); + // handle rollback or advance registry to the next block + let block_number = block.number as u32; + if block.status == BlockStatus::RolledBack { + if let Err(e) = utxo_registry.rollback_before(block_number) { + error!("rollback_before({}) failed: {}", block_number, e); + } + utxo_registry.next_block(); + state = history.lock().await.get_rolled_back_state(block.number); + current_block = Some(block.clone()); + } - if tracing::enabled!(tracing::Level::DEBUG) { - debug!( + let mut utxo_deltas = Vec::new(); + let mut asset_deltas = Vec::new(); + let mut cip25_metadata_updates = Vec::new(); + let mut withdrawals = Vec::new(); + let mut certificates = Vec::new(); + let mut voting_procedures = Vec::new(); + let mut proposal_procedures = Vec::new(); + let mut alonzo_babbage_update_proposals = Vec::new(); + let mut total_output: u128 = 0; + let mut total_fees: u64 = 0; + let total_txs = txs_msg.txs.len() as u64; + + let span = info_span!("tx_unpacker.handle_txs", block = block.number); + span.in_scope(|| { + for (tx_index, raw_tx) in txs_msg.txs.iter().enumerate() { + let tx_index = tx_index as u16; + + // Parse the tx + match MultiEraTx::decode(raw_tx) { + Ok(tx) => { + // Validate transaction + let _ = state.validate_transaction(block, &tx, &utxo_registry); + + let tx_hash: TxHash = + tx.hash().to_vec().try_into().expect("invalid tx hash length"); + let tx_identifier = TxIdentifier::new(block_number, tx_index); + + let certs = tx.certs(); + let tx_withdrawals = tx.withdrawals_sorted_set(); + let mut props = None; + let mut votes = None; + + let (tx_inputs, tx_outputs, errors) = + map_parameters::map_transaction_inputs_outputs( + block_number, + tx_index, + &tx, + ); + + if tracing::enabled!(tracing::Level::DEBUG) { + debug!( "Decoded tx with {} inputs, {} outputs, {} certs, {} errors", tx_inputs.len(), tx_outputs.len(), certs.len(), errors.len() ) - } - - if publish_utxo_deltas_topic.is_some() { - // Lookup and remove UTxOIdentifier from registry - // Group deltas by tx - let mut tx_utxo_deltas = TxUTxODeltas {tx_identifier, inputs: Vec::new(), outputs: Vec::new()}; - - // Remove inputs from UTxORegistry and push to UTxOIdentifiers to delta - for tx_ref in tx_inputs { - match utxo_registry.consume(&tx_ref) { - Ok(tx_identifier) => { - // Add TxInput to utxo_deltas - tx_utxo_deltas.inputs.push( - UTxOIdentifier::new( - tx_identifier.block_number(), - tx_identifier.tx_index(), - tx_ref.output_index, - ) - ); - } - Err(e) => { - error!("Failed to consume input {}: {e}", tx_ref.output_index); - } + } + + if publish_utxo_deltas_topic.is_some() { + // Lookup and remove UTxOIdentifier from registry + // Group deltas by tx + let mut tx_utxo_deltas = TxUTxODeltas { + tx_identifier, + inputs: Vec::new(), + outputs: Vec::new(), + }; + + // Remove inputs from UTxORegistry and push to UTxOIdentifiers to delta + for tx_ref in tx_inputs { + match utxo_registry.consume(&tx_ref) { + Ok(tx_identifier) => { + // Add TxInput to utxo_deltas + tx_utxo_deltas.inputs.push(UTxOIdentifier::new( + tx_identifier.block_number(), + tx_identifier.tx_index(), + tx_ref.output_index, + )); + } + Err(e) => { + error!( + "Failed to consume input {}: {e}", + tx_ref.output_index + ); } } + } - for (tx_ref, output) in tx_outputs.into_iter() { - total_output += output.value.coin() as u128; + for (tx_ref, output) in tx_outputs.into_iter() { + total_output += output.value.coin() as u128; - if let Err(e) = utxo_registry.add( - block_number, - tx_index, - tx_ref, - ) { - error!("Failed to insert output into registry: {e}"); - } - else { - tx_utxo_deltas.outputs.push(output); - } + if let Err(e) = + utxo_registry.add(block_number, tx_index, tx_ref) + { + error!("Failed to insert output into registry: {e}"); + } else { + tx_utxo_deltas.outputs.push(output); } - - utxo_deltas.push(tx_utxo_deltas); } - if publish_asset_deltas_topic.is_some() { - let mut tx_deltas: Vec<(PolicyId, Vec)> = Vec::new(); + utxo_deltas.push(tx_utxo_deltas); + } - // Mint deltas - for policy_group in tx.mints().iter() { - if let Some((policy_id, deltas)) = map_parameters::map_mint_burn(policy_group) { - tx_deltas.push((policy_id, deltas)); - } - } + if publish_asset_deltas_topic.is_some() { + let mut tx_deltas: Vec<(PolicyId, Vec)> = + Vec::new(); - if let Some(metadata) = tx.metadata().find(CIP25_METADATA_LABEL) { - let mut metadata_raw = Vec::new(); - match encode(metadata, &mut metadata_raw) { - Ok(()) => { - cip25_metadata_updates.push(metadata_raw); - } - Err(e) => { - error!("failed to encode CIP-25 metadatum: {e:#}"); - } - } + // Mint deltas + for policy_group in tx.mints().iter() { + if let Some((policy_id, deltas)) = + map_parameters::map_mint_burn(policy_group) + { + tx_deltas.push((policy_id, deltas)); } + } - if !tx_deltas.is_empty() { - asset_deltas.push((tx_identifier, tx_deltas)); + if let Some(metadata) = tx.metadata().find(CIP25_METADATA_LABEL) + { + let mut metadata_raw = Vec::new(); + match encode(metadata, &mut metadata_raw) { + Ok(()) => { + cip25_metadata_updates.push(metadata_raw); + } + Err(e) => { + error!("failed to encode CIP-25 metadatum: {e:#}"); + } } } - if publish_certificates_topic.is_some() { - for ( cert_index, cert) in certs.iter().enumerate() { - match map_parameters::map_certificate(cert, tx_identifier, cert_index, network_id) { - Ok(tx_cert) => { - certificates.push(tx_cert); - }, - Err(_e) => { - // TODO error unexpected - //error!("{e}"); - } + if !tx_deltas.is_empty() { + asset_deltas.push((tx_identifier, tx_deltas)); + } + } + + if publish_certificates_topic.is_some() { + for (cert_index, cert) in certs.iter().enumerate() { + match map_parameters::map_certificate( + cert, + tx_identifier, + cert_index, + network_id, + ) { + Ok(tx_cert) => { + certificates.push(tx_cert); + } + Err(_e) => { + // TODO error unexpected + //error!("{e}"); } } } - - if publish_withdrawals_topic.is_some() { - for (key, value) in tx_withdrawals { - match StakeAddress::from_binary(key) { - Ok(stake_address) => { - withdrawals.push(Withdrawal { - address: stake_address, - value, - tx_identifier - }); - } - Err(e) => error!("Bad stake address: {e:#}"), + } + + if publish_withdrawals_topic.is_some() { + for (key, value) in tx_withdrawals { + match StakeAddress::from_binary(key) { + Ok(stake_address) => { + withdrawals.push(Withdrawal { + address: stake_address, + value, + tx_identifier, + }); } + Err(e) => error!("Bad stake address: {e:#}"), } } - - if publish_governance_procedures_topic.is_some() { - //Self::decode_legacy_updates(&mut legacy_update_proposals, &block, &raw_tx); - if block.era >= Era::Shelley && block.era < Era::Babbage { - if let Ok(alonzo) = MultiEraTx::decode_for_era(traverse::Era::Alonzo, raw_tx) { - if let Some(update) = alonzo.update() { - if let Some(alonzo_update) = update.as_alonzo() { - Self::decode_updates( + } + + if publish_governance_procedures_topic.is_some() { + //Self::decode_legacy_updates(&mut legacy_update_proposals, &block, &raw_tx); + if block.era >= Era::Shelley && block.era < Era::Babbage { + if let Ok(alonzo) = MultiEraTx::decode_for_era( + traverse::Era::Alonzo, + raw_tx, + ) { + if let Some(update) = alonzo.update() { + if let Some(alonzo_update) = update.as_alonzo() { + Self::decode_updates( &mut alonzo_babbage_update_proposals, &alonzo_update.proposed_protocol_parameter_updates, alonzo_update.epoch, map_parameters::map_alonzo_protocol_param_update ); - } } } } - else if block.era >= Era::Babbage && block.era < Era::Conway{ - if let Ok(babbage) = MultiEraTx::decode_for_era(traverse::Era::Babbage, raw_tx) { - if let Some(update) = babbage.update() { - if let Some(babbage_update) = update.as_babbage() { - Self::decode_updates( + } else if block.era >= Era::Babbage && block.era < Era::Conway { + if let Ok(babbage) = MultiEraTx::decode_for_era( + traverse::Era::Babbage, + raw_tx, + ) { + if let Some(update) = babbage.update() { + if let Some(babbage_update) = update.as_babbage() { + Self::decode_updates( &mut alonzo_babbage_update_proposals, &babbage_update.proposed_protocol_parameter_updates, babbage_update.epoch, map_parameters::map_babbage_protocol_param_update ); - } } } } } + } - if let Some(conway) = tx.as_conway() { - if let Some(ref v) = conway.transaction_body.voting_procedures { - votes = Some(v); - } - - if let Some(ref p) = conway.transaction_body.proposal_procedures { - props = Some(p); - } + if let Some(conway) = tx.as_conway() { + if let Some(ref v) = conway.transaction_body.voting_procedures { + votes = Some(v); } - - if publish_governance_procedures_topic.is_some() { - if let Some(pp) = props { - // Nonempty set -- governance_message.proposal_procedures will not be empty - let mut proc_id = GovActionId { transaction_id: tx_hash, action_index: 0 }; - for (action_index, pallas_governance_proposals) in pp.iter().enumerate() { - match proc_id.set_action_index(action_index) + if let Some(ref p) = conway.transaction_body.proposal_procedures + { + props = Some(p); + } + } + + if publish_governance_procedures_topic.is_some() { + if let Some(pp) = props { + // Nonempty set -- governance_message.proposal_procedures will not be empty + let mut proc_id = GovActionId { + transaction_id: tx_hash, + action_index: 0, + }; + for (action_index, pallas_governance_proposals) in + pp.iter().enumerate() + { + match proc_id.set_action_index(action_index) .and_then (|proc_id| map_parameters::map_governance_proposals_procedures(proc_id, pallas_governance_proposals)) { Ok(g) => proposal_procedures.push(g), Err(e) => error!("Cannot decode governance proposal procedure {} idx {} in slot {}: {e}", proc_id, action_index, block.slot) } - } } + } - if let Some(pallas_vp) = votes { - // Nonempty set -- governance_message.voting_procedures will not be empty - match map_parameters::map_all_governance_voting_procedures(pallas_vp) { + if let Some(pallas_vp) = votes { + // Nonempty set -- governance_message.voting_procedures will not be empty + match map_parameters::map_all_governance_voting_procedures(pallas_vp) { Ok(vp) => voting_procedures.push((tx_hash, vp)), Err(e) => error!("Cannot decode governance voting procedures in slot {}: {e}", block.slot) } - } } + } - // Capture the fees - if let Some(fee) = tx.fee() { - total_fees += fee; - } - }, + // Capture the fees + if let Some(fee) = tx.fee() { + total_fees += fee; + } + } - Err(e) => error!("Can't decode transaction in slot {}: {e}", - block.slot) + Err(e) => { + error!("Can't decode transaction in slot {}: {e}", block.slot) } } + } + }); + + utxo_registry.next_block(); + + // Publish messages in parallel + let mut futures = Vec::new(); + if let Some(ref topic) = publish_utxo_deltas_topic { + let msg = Message::Cardano(( + block.clone(), + CardanoMessage::UTXODeltas(UTXODeltasMessage { + deltas: utxo_deltas, + }), + )); + + futures.push(context.message_bus.publish(topic, Arc::new(msg))); + } - utxo_registry.next_block(); + if let Some(ref topic) = publish_asset_deltas_topic { + let msg = Message::Cardano(( + block.clone(), + CardanoMessage::AssetDeltas(AssetDeltasMessage { + deltas: asset_deltas, + cip25_metadata_updates, + }), + )); - // Publish messages in parallel - let mut futures = Vec::new(); - if let Some(ref topic) = publish_utxo_deltas_topic { - let msg = Message::Cardano(( - block.clone(), - CardanoMessage::UTXODeltas(UTXODeltasMessage { - deltas: utxo_deltas, - }) - )); + futures.push(context.message_bus.publish(topic, Arc::new(msg))); + } - futures.push(context.message_bus.publish(topic, Arc::new(msg))); - } + if let Some(ref topic) = publish_withdrawals_topic { + let msg = Message::Cardano(( + block.clone(), + CardanoMessage::Withdrawals(WithdrawalsMessage { withdrawals }), + )); - if let Some(ref topic) = publish_asset_deltas_topic { - let msg = Message::Cardano(( - block.clone(), - CardanoMessage::AssetDeltas(AssetDeltasMessage { - deltas: asset_deltas, - cip25_metadata_updates - }) - )); + futures.push(context.message_bus.publish(topic, Arc::new(msg))); + } - futures.push(context.message_bus.publish(topic, Arc::new(msg))); - } + if let Some(ref topic) = publish_certificates_topic { + let msg = Message::Cardano(( + block.clone(), + CardanoMessage::TxCertificates(TxCertificatesMessage { certificates }), + )); - if let Some(ref topic) = publish_withdrawals_topic { - let msg = Message::Cardano(( - block.clone(), - CardanoMessage::Withdrawals(WithdrawalsMessage { - withdrawals, - }) - )); + futures.push(context.message_bus.publish(topic, Arc::new(msg))); + } - futures.push(context.message_bus.publish(topic, Arc::new(msg))); - } + if let Some(ref topic) = publish_governance_procedures_topic { + let governance_msg = Arc::new(Message::Cardano(( + block.clone(), + CardanoMessage::GovernanceProcedures(GovernanceProceduresMessage { + voting_procedures, + proposal_procedures, + alonzo_babbage_updates: alonzo_babbage_update_proposals, + }), + ))); + + futures.push(context.message_bus.publish(topic, governance_msg.clone())); + } - if let Some(ref topic) = publish_certificates_topic { - let msg = Message::Cardano(( - block.clone(), - CardanoMessage::TxCertificates(TxCertificatesMessage { - certificates, - }) - )); + if let Some(ref topic) = publish_block_txs_topic { + let msg = Message::Cardano(( + block.clone(), + CardanoMessage::BlockInfoMessage(BlockTxsMessage { + total_txs, + total_output, + total_fees, + }), + )); + + futures.push(context.message_bus.publish(topic, Arc::new(msg))); + } - futures.push(context.message_bus.publish(topic, Arc::new(msg))); - } + join_all(futures) + .await + .into_iter() + .filter_map(Result::err) + .for_each(|e| error!("Failed to publish: {e}")); + } - if let Some(ref topic) = publish_governance_procedures_topic { - let governance_msg = Arc::new(Message::Cardano(( - block.clone(), - CardanoMessage::GovernanceProcedures( - GovernanceProceduresMessage { - voting_procedures, - proposal_procedures, - alonzo_babbage_updates: alonzo_babbage_update_proposals - }) - ))); - - futures.push(context.message_bus.publish(topic, - governance_msg.clone())); - } + _ => error!("Unexpected message type: {message:?}"), + } - if let Some(ref topic) = publish_block_txs_topic { - let msg = Message::Cardano(( - block.clone(), - CardanoMessage::BlockInfoMessage(BlockTxsMessage { - total_txs, - total_output, - total_fees - }) - )); - - futures.push(context.message_bus.publish(topic, Arc::new(msg))); - } + if new_epoch { + let (_, protocol_parameters_msg) = protocol_params_sub.read().await?; + if let Message::Cardano((block_info, CardanoMessage::ProtocolParams(params))) = + protocol_parameters_msg.as_ref() + { + Self::check_sync(¤t_block, block_info); + let span = info_span!( + "tx_unpacker.handle_protocol_params", + block = block_info.number + ); + span.in_scope(|| { + state.handle_protocol_params(params); + }); + } + } - join_all(futures) - .await - .into_iter() - .filter_map(Result::err) - .for_each(|e| error!("Failed to publish: {e}")); - }.instrument(span).await; - } + // Commit the new state + if let Some(block_info) = current_block { + history.lock().await.commit(block_info.number, state); + } + } + } - _ => error!("Unexpected message type: {message:?}") + fn decode_updates( + dest: &mut Vec, + proposals: &KeyValuePairs, + epoch: u64, + map: impl Fn(&EraSpecificUpdateProposals) -> Result>, + ) { + let mut update = AlonzoBabbageUpdateProposal { + proposals: Vec::new(), + enactment_epoch: epoch, + }; + + for (hash_bytes, vote) in proposals.iter() { + let hash = match GenesisKeyhash::try_from(hash_bytes.as_ref()) { + Ok(h) => h, + Err(e) => { + error!("Invalid genesis keyhash in protocol parameter update: {e}"); + continue; } + }; + + match map(vote) { + Ok(upd) => update.proposals.push((hash, upd)), + Err(e) => error!("Cannot convert protocol param update {vote:?}: {e}"), } + } + + dest.push(update); + } + + /// Main init function + pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { + // Publishers + let publish_utxo_deltas_topic = config.get_string("publish-utxo-deltas-topic").ok(); + if let Some(ref topic) = publish_utxo_deltas_topic { + info!("Publishing UTXO deltas on '{topic}'"); + } + + let publish_asset_deltas_topic = config.get_string("publish-asset-deltas-topic").ok(); + if let Some(ref topic) = publish_asset_deltas_topic { + info!("Publishing native asset deltas on '{topic}'"); + } + + let publish_withdrawals_topic = config.get_string("publish-withdrawals-topic").ok(); + if let Some(ref topic) = publish_withdrawals_topic { + info!("Publishing withdrawals on '{topic}'"); + } + + let publish_certificates_topic = config.get_string("publish-certificates-topic").ok(); + if let Some(ref topic) = publish_certificates_topic { + info!("Publishing certificates on '{topic}'"); + } + + let publish_governance_procedures_topic = + config.get_string("publish-governance-topic").ok(); + if let Some(ref topic) = publish_governance_procedures_topic { + info!("Publishing governance procedures on '{topic}'"); + } + + let publish_block_txs_topic = config.get_string("publish-block-txs-topic").ok(); + if let Some(ref topic) = publish_block_txs_topic { + info!("Publishing block txs on '{topic}'"); + } + + // Subscribers + let genesis_utxos_subscribe_topic = config + .get_string("genesis-utxos-subscribe-topic") + .unwrap_or(DEFAULT_GENESIS_SUBSCRIBE_TOPIC.to_string()); + info!("Creating subscriber on '{genesis_utxos_subscribe_topic}'"); + + let transactions_subscribe_topic = config + .get_string("subscribe-topic") + .unwrap_or(DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC.to_string()); + info!("Creating subscriber on '{transactions_subscribe_topic}'"); + + let protocol_params_subscribe_topic = config + .get_string("protocol-params-subscribe-topic") + .unwrap_or(DEFAULT_PROTOCOL_PARAMS_SUBSCRIBE_TOPIC.to_string()); + info!("Creating subscriber on '{protocol_params_subscribe_topic}'"); + + let genesis_sub = context.subscribe(&genesis_utxos_subscribe_topic).await?; + let txs_sub = context.subscribe(&transactions_subscribe_topic).await?; + let protocol_params_sub = context.subscribe(&protocol_params_subscribe_topic).await?; + + let network_id: NetworkId = + config.get_string("network-id").unwrap_or("mainnet".to_string()).into(); + + // Initialize State + let history = Arc::new(Mutex::new(StateHistory::::new( + "tx_unpacker", + StateHistoryStore::default_block_store(), + ))); + + // Initialize UTxORegistry + let utxo_registry = UTxORegistry::default(); + + let context_run = context.clone(); + context.run(async move { + Self::run( + context_run, + network_id, + history, + utxo_registry, + publish_utxo_deltas_topic, + publish_asset_deltas_topic, + publish_withdrawals_topic, + publish_certificates_topic, + publish_governance_procedures_topic, + publish_block_txs_topic, + genesis_sub, + txs_sub, + protocol_params_sub, + ) + .await + .unwrap_or_else(|e| error!("Failed to run Tx Unpacker: {e}")); }); Ok(()) } + + /// Check for synchronisation + fn check_sync(expected: &Option, actual: &BlockInfo) { + if let Some(ref block) = expected { + if block.number != actual.number { + error!( + expected = block.number, + actual = actual.number, + "Messages out of sync" + ); + } + } + } } diff --git a/modules/tx_unpacker/src/validations/mod.rs b/modules/tx_unpacker/src/validations/mod.rs index ea3e3b8b..d9f27afd 100644 --- a/modules/tx_unpacker/src/validations/mod.rs +++ b/modules/tx_unpacker/src/validations/mod.rs @@ -1 +1,2 @@ pub mod shelley; +pub use self::shelley::*; diff --git a/modules/tx_unpacker/src/validations/shelley/mod.rs b/modules/tx_unpacker/src/validations/shelley/mod.rs index 5b379e46..c0725f71 100644 --- a/modules/tx_unpacker/src/validations/shelley/mod.rs +++ b/modules/tx_unpacker/src/validations/shelley/mod.rs @@ -1 +1,21 @@ +use acropolis_common::{ + protocol_params::ShelleyParams, validation::TransactionValidationError, TxIdentifier, TxOutRef, +}; +use anyhow::Result; +use pallas::ledger::traverse::MultiEraTx; + pub mod utxo; + +pub fn validate_shelley_tx( + tx: &MultiEraTx, + shelley_params: &ShelleyParams, + current_slot: u64, + lookup_by_hash: F, +) -> Result<(), TransactionValidationError> +where + F: Fn(TxOutRef) -> Result, +{ + utxo::validate_shelley_tx(tx, shelley_params, current_slot, lookup_by_hash).map_err(|e| *e)?; + + Ok(()) +} diff --git a/modules/tx_unpacker/src/validations/shelley/utxo.rs b/modules/tx_unpacker/src/validations/shelley/utxo.rs index 7df6e74f..bbbfe6f8 100644 --- a/modules/tx_unpacker/src/validations/shelley/utxo.rs +++ b/modules/tx_unpacker/src/validations/shelley/utxo.rs @@ -16,20 +16,6 @@ use pallas::{ }, }; -fn get_alonzo_comp_tx_size(mtx: &alonzo::MintedTx) -> u32 { - match &mtx.auxiliary_data { - pallas_codec::utils::Nullable::Some(aux_data) => { - (aux_data.raw_cbor().len() - + mtx.transaction_body.raw_cbor().len() - + mtx.transaction_witness_set.raw_cbor().len()) as u32 - } - _ => { - (mtx.transaction_body.raw_cbor().len() + mtx.transaction_witness_set.raw_cbor().len()) - as u32 - } - } -} - fn get_lovelace_from_alonzo_value(val: &alonzo::Value) -> Lovelace { match val { alonzo::Value::Coin(res) => *res, diff --git a/processes/omnibus/omnibus.toml b/processes/omnibus/omnibus.toml index dd56a011..4b25d34e 100644 --- a/processes/omnibus/omnibus.toml +++ b/processes/omnibus/omnibus.toml @@ -173,8 +173,6 @@ store-totals = false # Enables /addresses/{address}/transactions endpoint store-transactions = false -[module.tx-validator-phase1] - [module.block-vrf-validator] [module.block-kes-validator] From de9ed93c0050bce7f1111042d7d8466b0c303f5e Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Mon, 1 Dec 2025 02:07:13 +0100 Subject: [PATCH 17/18] fix: decode transaction for specific era --- common/src/address.rs | 30 ++++- modules/accounts_state/src/verifier.rs | 16 ++- modules/address_state/src/address_state.rs | 33 +++-- modules/address_state/src/state.rs | 5 +- .../block_kes_validator/src/ouroboros/kes.rs | 5 +- modules/drep_state/src/drep_state.rs | 124 ++++++++---------- modules/governance_state/src/conway_voting.rs | 9 +- .../governance_state/src/governance_state.rs | 39 +++--- .../src/mithril_snapshot_fetcher.rs | 8 +- modules/mithril_snapshot_fetcher/src/pause.rs | 5 +- .../rest_blockfrost/src/handlers/epochs.rs | 29 ++-- modules/tx_unpacker/src/state.rs | 12 +- modules/tx_unpacker/src/tx_unpacker.rs | 8 +- .../src/validations/shelley/mod.rs | 8 +- .../src/validations/shelley/utxo.rs | 17 ++- .../context.json | 2 +- .../{wrong_ttl.cbor => expired_utxo.cbor} | 0 .../input_set_empty_utxo.cbor | 1 + .../context.json | 47 +++++++ .../tx.cbor | 1 + 20 files changed, 234 insertions(+), 165 deletions(-) rename modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/{wrong_ttl.cbor => expired_utxo.cbor} (100%) create mode 100644 modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/input_set_empty_utxo.cbor create mode 100644 modules/tx_unpacker/tests/data/cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f/context.json create mode 100644 modules/tx_unpacker/tests/data/cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f/tx.cbor diff --git a/common/src/address.rs b/common/src/address.rs index f770d2c1..0d376cce 100644 --- a/common/src/address.rs +++ b/common/src/address.rs @@ -776,7 +776,10 @@ mod tests { }); let text = address.to_string().unwrap(); - assert_eq!(text, "addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x"); + assert_eq!( + text, + "addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x" + ); let unpacked = Address::from_string(&text).unwrap(); assert_eq!(address, unpacked); @@ -791,7 +794,10 @@ mod tests { }); let text = address.to_string().unwrap(); - assert_eq!(text, "addr1z8phkx6acpnf78fuvxn0mkew3l0fd058hzquvz7w36x4gten0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs9yc0hh"); + assert_eq!( + text, + "addr1z8phkx6acpnf78fuvxn0mkew3l0fd058hzquvz7w36x4gten0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs9yc0hh" + ); let unpacked = Address::from_string(&text).unwrap(); assert_eq!(address, unpacked); @@ -806,7 +812,10 @@ mod tests { }); let text = address.to_string().unwrap(); - assert_eq!(text, "addr1yx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerkr0vd4msrxnuwnccdxlhdjar77j6lg0wypcc9uar5d2shs2z78ve"); + assert_eq!( + text, + "addr1yx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzerkr0vd4msrxnuwnccdxlhdjar77j6lg0wypcc9uar5d2shs2z78ve" + ); let unpacked = Address::from_string(&text).unwrap(); assert_eq!(address, unpacked); @@ -821,7 +830,10 @@ mod tests { }); let text = address.to_string().unwrap(); - assert_eq!(text, "addr1x8phkx6acpnf78fuvxn0mkew3l0fd058hzquvz7w36x4gt7r0vd4msrxnuwnccdxlhdjar77j6lg0wypcc9uar5d2shskhj42g"); + assert_eq!( + text, + "addr1x8phkx6acpnf78fuvxn0mkew3l0fd058hzquvz7w36x4gt7r0vd4msrxnuwnccdxlhdjar77j6lg0wypcc9uar5d2shskhj42g" + ); let unpacked = Address::from_string(&text).unwrap(); assert_eq!(address, unpacked); @@ -935,8 +947,14 @@ mod tests { #[test] fn shelley_to_stake_address_string_mainnet() { - let normal_address = ShelleyAddress::from_string("addr1q82peck5fynytkgjsp9vnpul59zswsd4jqnzafd0mfzykma625r684xsx574ltpznecr9cnc7n9e2hfq9lyart3h5hpszffds5").expect("valid normal address"); - let script_address = ShelleyAddress::from_string("addr1zx0whlxaw4ksygvuljw8jxqlw906tlql06ern0gtvvzhh0c6409492020k6xml8uvwn34wrexagjh5fsk5xk96jyxk2qhlj6gf").expect("valid script address"); + let normal_address = ShelleyAddress::from_string( + "addr1q82peck5fynytkgjsp9vnpul59zswsd4jqnzafd0mfzykma625r684xsx574ltpznecr9cnc7n9e2hfq9lyart3h5hpszffds5", + ) + .expect("valid normal address"); + let script_address = ShelleyAddress::from_string( + "addr1zx0whlxaw4ksygvuljw8jxqlw906tlql06ern0gtvvzhh0c6409492020k6xml8uvwn34wrexagjh5fsk5xk96jyxk2qhlj6gf", + ) + .expect("valid script address"); let normal_stake_address = normal_address .stake_address_string() diff --git a/modules/accounts_state/src/verifier.rs b/modules/accounts_state/src/verifier.rs index dabf5648..e875b5a6 100644 --- a/modules/accounts_state/src/verifier.rs +++ b/modules/accounts_state/src/verifier.rs @@ -238,13 +238,15 @@ impl Verifier { } Both(expected, actual) => { if expected.amount != actual.amount { - error!("Different reward: SPO {} account {} {:?} expected {}, actual {} ({})", - expected_spo.0, - expected.account, - expected.rtype, - expected.amount, - actual.amount, - actual.amount as i64-expected.amount as i64); + error!( + "Different reward: SPO {} account {} {:?} expected {}, actual {} ({})", + expected_spo.0, + expected.account, + expected.rtype, + expected.amount, + actual.amount, + actual.amount as i64 - expected.amount as i64 + ); errors += 1; } else { debug!( diff --git a/modules/address_state/src/address_state.rs b/modules/address_state/src/address_state.rs index ec746b0e..e72b34af 100644 --- a/modules/address_state/src/address_state.rs +++ b/modules/address_state/src/address_state.rs @@ -207,26 +207,23 @@ impl AddressState { let state = state_mutex.lock().await; let response = match query { - AddressStateQuery::GetAddressUTxOs { address } => { - match state.get_address_utxos(address).await { - Ok(Some(utxos)) => AddressStateQueryResponse::AddressUTxOs(utxos), - Ok(None) => match address.to_string() { - Ok(addr_str) => { - AddressStateQueryResponse::Error(QueryError::not_found( - format!("Address {} not found", addr_str), - )) - } - Err(e) => { - AddressStateQueryResponse::Error(QueryError::internal_error( - format!("Could not convert address to string: {}", e), - )) - } - }, + AddressStateQuery::GetAddressUTxOs { address } => match state + .get_address_utxos(address) + .await + { + Ok(Some(utxos)) => AddressStateQueryResponse::AddressUTxOs(utxos), + Ok(None) => match address.to_string() { + Ok(addr_str) => AddressStateQueryResponse::Error( + QueryError::not_found(format!("Address {} not found", addr_str)), + ), Err(e) => AddressStateQueryResponse::Error(QueryError::internal_error( - e.to_string(), + format!("Could not convert address to string: {}", e), )), - } - } + }, + Err(e) => AddressStateQueryResponse::Error(QueryError::internal_error( + e.to_string(), + )), + }, AddressStateQuery::GetAddressTransactions { address } => { match state.get_address_transactions(address).await { Ok(Some(txs)) => AddressStateQueryResponse::AddressTransactions(txs), diff --git a/modules/address_state/src/state.rs b/modules/address_state/src/state.rs index 350f4e13..abfbcc8e 100644 --- a/modules/address_state/src/state.rs +++ b/modules/address_state/src/state.rs @@ -237,7 +237,10 @@ mod tests { use tempfile::tempdir; fn dummy_address() -> Address { - Address::from_string("DdzFFzCqrht7fNAHwdou7iXPJ5NZrssAH53yoRMUtF9t6momHH52EAxM5KmqDwhrjT7QsHjbMPJUBywmzAgmF4hj2h9eKj4U6Ahandyy").unwrap() + Address::from_string( + "DdzFFzCqrht7fNAHwdou7iXPJ5NZrssAH53yoRMUtF9t6momHH52EAxM5KmqDwhrjT7QsHjbMPJUBywmzAgmF4hj2h9eKj4U6Ahandyy", + ) + .unwrap() } fn test_config() -> AddressStorageConfig { diff --git a/modules/block_kes_validator/src/ouroboros/kes.rs b/modules/block_kes_validator/src/ouroboros/kes.rs index 0ecbfb2d..802a2c66 100644 --- a/modules/block_kes_validator/src/ouroboros/kes.rs +++ b/modules/block_kes_validator/src/ouroboros/kes.rs @@ -68,7 +68,10 @@ impl Signature { impl From<&[u8; Self::SIZE]> for Signature { fn from(bytes: &[u8; Self::SIZE]) -> Self { Signature(Sum6KesSig::from_bytes(bytes).unwrap_or_else(|e| { - unreachable!("Impossible! Failed to create a KES signature from a slice ({}) of known size: {e:?}", hex::encode(bytes)) + unreachable!( + "Impossible! Failed to create a KES signature from a slice ({}) of known size: {e:?}", + hex::encode(bytes) + ) })) } } diff --git a/modules/drep_state/src/drep_state.rs b/modules/drep_state/src/drep_state.rs index b1de308d..98b87130 100644 --- a/modules/drep_state/src/drep_state.rs +++ b/modules/drep_state/src/drep_state.rs @@ -332,75 +332,65 @@ impl DRepState { ), } } - GovernanceStateQuery::GetDRepDelegators { drep_credential } => { - match locked.current() { - Some(state) => match state.get_drep_delegators(drep_credential) { - Ok(Some(delegators)) => { - GovernanceStateQueryResponse::DRepDelegators( - DRepDelegatorAddresses { - addresses: delegators.clone(), - }, - ) - } - Ok(None) => GovernanceStateQueryResponse::Error( - QueryError::not_found(format!( - "DRep delegators for {:?} not found", - drep_credential - )), - ), - Err(msg) => GovernanceStateQueryResponse::Error( - QueryError::internal_error(msg), - ), - }, - None => GovernanceStateQueryResponse::Error( - QueryError::internal_error("No current state"), + GovernanceStateQuery::GetDRepDelegators { drep_credential } => match locked + .current() + { + Some(state) => match state.get_drep_delegators(drep_credential) { + Ok(Some(delegators)) => GovernanceStateQueryResponse::DRepDelegators( + DRepDelegatorAddresses { + addresses: delegators.clone(), + }, ), - } - } - GovernanceStateQuery::GetDRepMetadata { drep_credential } => { - match locked.current() { - Some(state) => match state.get_drep_anchor(drep_credential) { - Ok(Some(anchor)) => GovernanceStateQueryResponse::DRepMetadata( - Some(Some(anchor.clone())), - ), - Ok(None) => GovernanceStateQueryResponse::Error( - QueryError::not_found(format!( - "DRep metadata for {:?} not found", - drep_credential - )), - ), - Err(msg) => GovernanceStateQueryResponse::Error( - QueryError::internal_error(msg), - ), - }, - None => GovernanceStateQueryResponse::Error( - QueryError::internal_error("No current state"), - ), - } - } + Ok(None) => GovernanceStateQueryResponse::Error(QueryError::not_found( + format!("DRep delegators for {:?} not found", drep_credential), + )), + Err(msg) => { + GovernanceStateQueryResponse::Error(QueryError::internal_error(msg)) + } + }, + None => GovernanceStateQueryResponse::Error(QueryError::internal_error( + "No current state", + )), + }, + GovernanceStateQuery::GetDRepMetadata { drep_credential } => match locked + .current() + { + Some(state) => match state.get_drep_anchor(drep_credential) { + Ok(Some(anchor)) => GovernanceStateQueryResponse::DRepMetadata(Some( + Some(anchor.clone()), + )), + Ok(None) => GovernanceStateQueryResponse::Error(QueryError::not_found( + format!("DRep metadata for {:?} not found", drep_credential), + )), + Err(msg) => { + GovernanceStateQueryResponse::Error(QueryError::internal_error(msg)) + } + }, + None => GovernanceStateQueryResponse::Error(QueryError::internal_error( + "No current state", + )), + }, - GovernanceStateQuery::GetDRepUpdates { drep_credential } => { - match locked.current() { - Some(state) => match state.get_drep_updates(drep_credential) { - Ok(Some(updates)) => { - GovernanceStateQueryResponse::DRepUpdates(DRepUpdates { - updates: updates.to_vec(), - }) - } - Ok(None) => { - GovernanceStateQueryResponse::Error(QueryError::not_found( - format!("DRep updates for {:?} not found", drep_credential), - )) - } - Err(msg) => GovernanceStateQueryResponse::Error( - QueryError::internal_error(msg), - ), - }, - None => GovernanceStateQueryResponse::Error( - QueryError::internal_error("No current state"), - ), - } - } + GovernanceStateQuery::GetDRepUpdates { drep_credential } => match locked + .current() + { + Some(state) => match state.get_drep_updates(drep_credential) { + Ok(Some(updates)) => { + GovernanceStateQueryResponse::DRepUpdates(DRepUpdates { + updates: updates.to_vec(), + }) + } + Ok(None) => GovernanceStateQueryResponse::Error(QueryError::not_found( + format!("DRep updates for {:?} not found", drep_credential), + )), + Err(msg) => { + GovernanceStateQueryResponse::Error(QueryError::internal_error(msg)) + } + }, + None => GovernanceStateQueryResponse::Error(QueryError::internal_error( + "No current state", + )), + }, GovernanceStateQuery::GetDRepVotes { drep_credential } => { match locked.current() { Some(state) => match state.get_drep_votes(drep_credential) { diff --git a/modules/governance_state/src/conway_voting.rs b/modules/governance_state/src/conway_voting.rs index 41f2064f..cf546281 100644 --- a/modules/governance_state/src/conway_voting.rs +++ b/modules/governance_state/src/conway_voting.rs @@ -154,8 +154,13 @@ impl ConwayVoting { // Re-voting is allowed; new vote must be treated as the proper one, // older is to be discarded. if tracing::enabled!(tracing::Level::DEBUG) { - debug!("Governance vote by {} for {} already registered! New: {:?}, old: {:?} from {}", - voter, action_id, procedure, prev_vote, prev_trans.encode_hex::() + debug!( + "Governance vote by {} for {} already registered! New: {:?}, old: {:?} from {}", + voter, + action_id, + procedure, + prev_vote, + prev_trans.encode_hex::() ); } } diff --git a/modules/governance_state/src/governance_state.rs b/modules/governance_state/src/governance_state.rs index 80d36fc5..4132c2be 100644 --- a/modules/governance_state/src/governance_state.rs +++ b/modules/governance_state/src/governance_state.rs @@ -195,18 +195,16 @@ impl GovernanceState { GovernanceStateQueryResponse::ProposalsList(ProposalsList { proposals }) } - GovernanceStateQuery::GetProposalInfo { proposal } => { - match locked.get_proposal(proposal) { - Some(proc) => { - GovernanceStateQueryResponse::ProposalInfo(ProposalInfo { - procedure: proc.clone(), - }) - } - None => GovernanceStateQueryResponse::Error(QueryError::not_found( - format!("Proposal {} not found", proposal), - )), - } - } + GovernanceStateQuery::GetProposalInfo { proposal } => match locked + .get_proposal(proposal) + { + Some(proc) => GovernanceStateQueryResponse::ProposalInfo(ProposalInfo { + procedure: proc.clone(), + }), + None => GovernanceStateQueryResponse::Error(QueryError::not_found( + format!("Proposal {} not found", proposal), + )), + }, GovernanceStateQuery::GetProposalVotes { proposal } => { match locked.get_proposal_votes(proposal) { Ok(votes) => { @@ -249,9 +247,7 @@ impl GovernanceState { if blk_g.new_epoch { let (blk_p, params) = Self::read_parameters(&mut protocol_s).await?; if blk_g != blk_p { - error!( - "Governance {blk_g:?} and protocol parameters {blk_p:?} are out of sync" - ); + error!("Governance {blk_g:?} and protocol parameters {blk_p:?} are out of sync"); } { @@ -267,16 +263,11 @@ impl GovernanceState { let (blk_spo, d_spo) = Self::read_spo(&mut spo_s).await?; if blk_g != blk_spo { - error!( - "Governance {blk_g:?} and SPO distribution {blk_spo:?} are out of sync" - ); + error!("Governance {blk_g:?} and SPO distribution {blk_spo:?} are out of sync"); } if blk_spo.epoch != d_spo.epoch + 1 { - error!( - "SPO distibution {blk_spo:?} != SPO epoch + 1 ({})", - d_spo.epoch - ); + error!("SPO distibution {blk_spo:?} != SPO epoch + 1 ({})", d_spo.epoch); } state.lock().await.handle_drep_stake(&d_drep, &d_spo).await? @@ -287,7 +278,9 @@ impl GovernanceState { } } Ok::<(), anyhow::Error>(()) - }.instrument(span).await?; + } + .instrument(span) + .await?; } } diff --git a/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs b/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs index 1a1a91e1..1d74304f 100644 --- a/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs +++ b/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs @@ -160,9 +160,7 @@ impl MithrilSnapshotFetcher { true } } else { - info!( - "SKIP DOWNLOAD: Snapshot is not expired by download max age: {download_max_age} hours" - ); + info!("SKIP DOWNLOAD: Snapshot is not expired by download max age: {download_max_age} hours"); true } } @@ -439,9 +437,7 @@ impl MithrilSnapshotFetcher { /// Async helper to prompt user for pause behavior async fn prompt_pause(description: String) -> bool { - info!( - "Paused at {description}. Press [Enter] to step to to the next, or [c + Enter] to continue without pauses." - ); + info!("Paused at {description}. Press [Enter] to step to to the next, or [c + Enter] to continue without pauses."); tokio::task::spawn_blocking(|| { use std::io::{self, BufRead}; let stdin = io::stdin(); diff --git a/modules/mithril_snapshot_fetcher/src/pause.rs b/modules/mithril_snapshot_fetcher/src/pause.rs index e1abb649..ac8f0dd7 100644 --- a/modules/mithril_snapshot_fetcher/src/pause.rs +++ b/modules/mithril_snapshot_fetcher/src/pause.rs @@ -28,7 +28,10 @@ impl PauseType { let parts: Vec<&str> = pause_str.split(':').collect(); if parts.len() != 2 { - error!("Invalid pause format: {}. Expected format: 'type:value' (e.g., 'epoch:214', 'block:1200')", pause_str); + error!( + "Invalid pause format: {}. Expected format: 'type:value' (e.g., 'epoch:214', 'block:1200')", + pause_str + ); return None; } diff --git a/modules/rest_blockfrost/src/handlers/epochs.rs b/modules/rest_blockfrost/src/handlers/epochs.rs index 5cb9e37e..a16723a9 100644 --- a/modules/rest_blockfrost/src/handlers/epochs.rs +++ b/modules/rest_blockfrost/src/handlers/epochs.rs @@ -117,9 +117,9 @@ pub async fn handle_epoch_info_blockfrost( Message::StateQueryResponse(StateQueryResponse::Accounts( AccountsStateQueryResponse::ActiveStakes(total_active_stake), )) => Ok(Some(total_active_stake)), - Message::StateQueryResponse(StateQueryResponse::Accounts( - AccountsStateQueryResponse::Error(_), - )) => Ok(None), + Message::StateQueryResponse(StateQueryResponse::Accounts(AccountsStateQueryResponse::Error(_))) => { + Ok(None) + } _ => Err(QueryError::internal_error( "Unexpected message type while retrieving the latest total active stakes", )), @@ -129,9 +129,7 @@ pub async fn handle_epoch_info_blockfrost( } else { // Historical epoch: use SPDD if available let total_active_stakes_msg = Arc::new(Message::StateQuery(StateQuery::SPDD( - SPDDStateQuery::GetEpochTotalActiveStakes { - epoch: epoch_number, - }, + SPDDStateQuery::GetEpochTotalActiveStakes { epoch: epoch_number }, ))); query_state( &context, @@ -139,18 +137,17 @@ pub async fn handle_epoch_info_blockfrost( total_active_stakes_msg, |message| match message { Message::StateQueryResponse(StateQueryResponse::SPDD( - SPDDStateQueryResponse::EpochTotalActiveStakes(total_active_stakes), - )) => Ok(Some(total_active_stakes)), - Message::StateQueryResponse(StateQueryResponse::SPDD( - SPDDStateQueryResponse::Error(_), - )) => Ok(None), - _ => Err(QueryError::internal_error( - format!("Unexpected message type while retrieving total active stakes for epoch: {epoch_number}"), - )), + SPDDStateQueryResponse::EpochTotalActiveStakes(total_active_stakes), + )) => Ok(Some(total_active_stakes)), + Message::StateQueryResponse(StateQueryResponse::SPDD(SPDDStateQueryResponse::Error(_))) => Ok(None), + _ => Err(QueryError::internal_error(format!( + "Unexpected message type while retrieving total active stakes for epoch: {epoch_number}" + ))), }, ) - .await? - }.unwrap_or(0); + .await? + } + .unwrap_or(0); if total_active_stakes == 0 { response.active_stake = None; diff --git a/modules/tx_unpacker/src/state.rs b/modules/tx_unpacker/src/state.rs index 05a00b1e..4d1966e0 100644 --- a/modules/tx_unpacker/src/state.rs +++ b/modules/tx_unpacker/src/state.rs @@ -4,7 +4,6 @@ use acropolis_common::{ validation::TransactionValidationError, BlockInfo, Era, }; use anyhow::Result; -use pallas::ledger::traverse::MultiEraTx; #[derive(Default, Clone)] pub struct State { @@ -25,7 +24,7 @@ impl State { pub fn validate_transaction( &self, block_info: &BlockInfo, - tx: &MultiEraTx, + raw_tx: &[u8], utxo_registry: &UTxORegistry, ) -> Result<(), TransactionValidationError> { match block_info.era { @@ -35,9 +34,12 @@ impl State { "Shelley params are not set".to_string(), )); }; - validations::validate_shelley_tx(tx, shelley_params, block_info.slot, |tx_ref| { - utxo_registry.lookup_by_hash(tx_ref) - }) + validations::validate_shelley_tx( + raw_tx, + shelley_params, + block_info.slot, + |tx_ref| utxo_registry.lookup_by_hash(tx_ref), + ) } _ => Ok(()), } diff --git a/modules/tx_unpacker/src/tx_unpacker.rs b/modules/tx_unpacker/src/tx_unpacker.rs index 9d239982..e310ac54 100644 --- a/modules/tx_unpacker/src/tx_unpacker.rs +++ b/modules/tx_unpacker/src/tx_unpacker.rs @@ -132,12 +132,14 @@ impl TxUnpacker { for (tx_index, raw_tx) in txs_msg.txs.iter().enumerate() { let tx_index = tx_index as u16; + // Validate transaction + if let Err(e) = state.validate_transaction(block, raw_tx, &utxo_registry) { + error!("Failed to validate transaction; tx_index={}, block={}: {e}", tx_index, block.number); + }; + // Parse the tx match MultiEraTx::decode(raw_tx) { Ok(tx) => { - // Validate transaction - let _ = state.validate_transaction(block, &tx, &utxo_registry); - let tx_hash: TxHash = tx.hash().to_vec().try_into().expect("invalid tx hash length"); let tx_identifier = TxIdentifier::new(block_number, tx_index); diff --git a/modules/tx_unpacker/src/validations/shelley/mod.rs b/modules/tx_unpacker/src/validations/shelley/mod.rs index c0725f71..96a8b318 100644 --- a/modules/tx_unpacker/src/validations/shelley/mod.rs +++ b/modules/tx_unpacker/src/validations/shelley/mod.rs @@ -2,12 +2,12 @@ use acropolis_common::{ protocol_params::ShelleyParams, validation::TransactionValidationError, TxIdentifier, TxOutRef, }; use anyhow::Result; -use pallas::ledger::traverse::MultiEraTx; +use pallas::ledger::traverse::{self, MultiEraTx}; pub mod utxo; pub fn validate_shelley_tx( - tx: &MultiEraTx, + raw_tx: &[u8], shelley_params: &ShelleyParams, current_slot: u64, lookup_by_hash: F, @@ -15,7 +15,9 @@ pub fn validate_shelley_tx( where F: Fn(TxOutRef) -> Result, { - utxo::validate_shelley_tx(tx, shelley_params, current_slot, lookup_by_hash).map_err(|e| *e)?; + let tx = MultiEraTx::decode_for_era(traverse::Era::Shelley, raw_tx) + .map_err(|e| TransactionValidationError::CborDecodeError(e.to_string()))?; + utxo::validate_shelley_tx(&tx, shelley_params, current_slot, lookup_by_hash).map_err(|e| *e)?; Ok(()) } diff --git a/modules/tx_unpacker/src/validations/shelley/utxo.rs b/modules/tx_unpacker/src/validations/shelley/utxo.rs index bbbfe6f8..00593274 100644 --- a/modules/tx_unpacker/src/validations/shelley/utxo.rs +++ b/modules/tx_unpacker/src/validations/shelley/utxo.rs @@ -15,6 +15,7 @@ use pallas::{ traverse::{Era as PallasEra, MultiEraTx}, }, }; +use tracing::error; fn get_lovelace_from_alonzo_value(val: &alonzo::Value) -> Lovelace { match val { @@ -58,6 +59,7 @@ where let mtx = match tx { MultiEraTx::AlonzoCompatible(mtx, PallasEra::Shelley) => mtx, _ => { + error!("Not a Shelley transaction: {:?}", tx); return Err(Box::new(UTxOValidationError::MalformedUTxO { era: Era::Shelley, reason: "Not a Shelley transaction".to_string(), @@ -287,19 +289,24 @@ pub fn validate_max_tx_size_utxo( mod tests { use super::*; use crate::{test_utils::TestContext, validation_fixture}; - use pallas::{codec as pallas_codec, ledger::primitives::alonzo::MintedTx as AlonzoMintedTx}; + use pallas::ledger::traverse; use test_case::test_case; + #[test_case(validation_fixture!("cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f") => + matches Ok(()); + )] #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e") => matches Ok(()); )] - #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "wrong_ttl") => + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "expired_utxo") => matches Err(UTxOValidationError::ExpiredUTxO { ttl: 7084747, current_slot: 7084748 }); )] + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "input_set_empty_utxo") => + matches Err(UTxOValidationError::InputSetEmptyUTxO); + )] #[allow(clippy::result_large_err)] fn shelley_test((ctx, raw_tx): (TestContext, Vec)) -> Result<(), UTxOValidationError> { - let alonzo_tx = pallas_codec::minicbor::decode::(&raw_tx).unwrap(); - let mtx = MultiEraTx::from_alonzo_compatible(&alonzo_tx, PallasEra::Shelley); + let tx = MultiEraTx::decode_for_era(traverse::Era::Shelley, &raw_tx).unwrap(); let lookup_by_hash = |tx_ref: TxOutRef| -> Result { ctx.utxos.get(&tx_ref).copied().ok_or_else(|| { @@ -309,7 +316,7 @@ mod tests { ) }) }; - validate_shelley_tx(&mtx, &ctx.shelley_params, ctx.current_slot, lookup_by_hash) + validate_shelley_tx(&tx, &ctx.shelley_params, ctx.current_slot, lookup_by_hash) .map_err(|e| *e) } } diff --git a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json index a468fefc..cd0e18fa 100644 --- a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json +++ b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json @@ -41,7 +41,7 @@ "utxos": [ [ ["278ec9a3dfb551288affd8d84b2d6c70b56a153ad1fd43e7b18a5528f687733e", 1], - [4616843, 12] + [4616843, 0] ] ] } diff --git a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/wrong_ttl.cbor b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/expired_utxo.cbor similarity index 100% rename from modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/wrong_ttl.cbor rename to modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/expired_utxo.cbor diff --git a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/input_set_empty_utxo.cbor b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/input_set_empty_utxo.cbor new file mode 100644 index 00000000..ec56ce5c --- /dev/null +++ b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/input_set_empty_utxo.cbor @@ -0,0 +1 @@ +84A40080018282584C82D818584283581CE8BD78295559F7EDD927190EC36F7FBE438F74CF0C9380C717AEB56FA101581E581C2B0B011BA3683D490846FD2A91BF44F73A59B1F93E5E753FC1AEBED1001A6363D6C21B000000BF32F3680F825839019902792B16C60946E986821C08CAD447C0C2292440F13F709A5CACFA1AD46761EE3214603CBFBC0CEBB6EDA23ADBDB36E8087A2A1C16765E1B0000000BA4324C40021A0002A1FD031A006C36C5A1028184582041A0B7A66D454ED0465E7B601D1FF1FE3CDDCDF5D127D1DA108CBB3555747CD158404C832202E28859CEFD3169EF238963A887A396E7773519B68721F9C059ABF7BEC56422111F4C66B13A116C2D10329A289C815FCE6D03F6BE903E74C3959BC703582066810329C45D09FD3E396A4EB1FF5E9D4632D5E156B5B320AD992C34D67E25F75822A101581E581C2B0B011BA3683D07B5F91D2AB76A8CAAB0E4DDA6099218E20432E4CCF5F6 \ No newline at end of file diff --git a/modules/tx_unpacker/tests/data/cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f/context.json b/modules/tx_unpacker/tests/data/cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f/context.json new file mode 100644 index 00000000..c5b010c6 --- /dev/null +++ b/modules/tx_unpacker/tests/data/cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f/context.json @@ -0,0 +1,47 @@ +{ + "current_slot": 4953800, + "shelley_params": { + "activeSlotsCoeff": 0.05, + "epochLength": 432000, + "maxKesEvolutions": 62, + "maxLovelaceSupply": 45000000000000000, + "networkId": "Mainnet", + "networkMagic": 764824073, + "protocolParams": { + "protocolVersion": { + "minor": 0, + "major": 2 + }, + "maxTxSize": 16384, + "maxBlockBodySize": 65536, + "maxBlockHeaderSize": 1100, + "keyDeposit": 2000000, + "minUTxOValue": 1000000, + "minFeeA": 44, + "minFeeB": 155381, + "poolDeposit": 500000000, + "nOpt": 150, + "minPoolCost": 340000000, + "eMax": 18, + "extraEntropy": { + "tag": "NeutralNonce" + }, + "decentralisationParam": 1, + "rho": 0.003, + "tau": 0.2, + "a0": 0.3 + }, + "securityParam": 2160, + "slotLength": 1, + "slotsPerKesPeriod": 129600, + "systemStart": "2017-09-23T21:44:51Z", + "updateQuorum": 5, + "genDelegs": {} + }, + "utxos": [ + [ + ["18ac56ec3b3495a9cab553c1589109c483784d2efeca1bc0c90a9218a1b5ed65", 1], + [3357485, 0] + ] + ] +} diff --git a/modules/tx_unpacker/tests/data/cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f/tx.cbor b/modules/tx_unpacker/tests/data/cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f/tx.cbor new file mode 100644 index 00000000..78dfa06d --- /dev/null +++ b/modules/tx_unpacker/tests/data/cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f/tx.cbor @@ -0,0 +1 @@ +84a4008182582018ac56ec3b3495a9cab553c1589109c483784d2efeca1bc0c90a9218a1b5ed65010181825839015d8ddb84e4d2595d55c7e719c7810af21073e927b2e3a9d512201764092f2bf6629b431e22b3fe826edf4d0593c155ca3e760df29afc7ed31a00b00f5a021a00028de1031a004bb2d4a10281845820bd33384a2f1de2d86a9cea56d1cffbc47b4b7f1b6c11b148a21939c24922a85658406970a2bd458f527440c6d231be0f6c22140d3ad5c36a134aa536e2baed7fe327f90edab37676bed02813186773f3ab997c2af437802b2886636cc3bdbb38d40d5820981f1fdcd3166d2a8d4d70b613a16ed81c7fcf3ee70565c9dcffb8dad29c104b41a0f5f6 \ No newline at end of file From 95a98df03f93c1f920623b8e85eaebddbf283136 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Mon, 1 Dec 2025 03:43:23 +0100 Subject: [PATCH 18/18] refactor: add more test cases and update test utils --- modules/tx_unpacker/src/test_utils.rs | 4 +- modules/tx_unpacker/src/tx_unpacker.rs | 2 + .../src/validations/shelley/utxo.rs | 40 +++++++++++++ .../bad_inputs_utxo.cbor | 1 + .../fee_too_small_utxo.cbor | 1 + .../max_tx_size_utxo.cbor | 1 + .../output_too_small_utxo.cbor | 1 + .../wrong_network.cbor | 1 + .../context.json | 59 +++++++++++++++++++ .../tx.cbor | 1 + .../wrong_network_withdrawal.cbor | 1 + 11 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/bad_inputs_utxo.cbor create mode 100644 modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/fee_too_small_utxo.cbor create mode 100644 modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/max_tx_size_utxo.cbor create mode 100644 modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/output_too_small_utxo.cbor create mode 100644 modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/wrong_network.cbor create mode 100644 modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/context.json create mode 100644 modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/tx.cbor create mode 100644 modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/wrong_network_withdrawal.cbor diff --git a/modules/tx_unpacker/src/test_utils.rs b/modules/tx_unpacker/src/test_utils.rs index 5193ffee..109773bf 100644 --- a/modules/tx_unpacker/src/test_utils.rs +++ b/modules/tx_unpacker/src/test_utils.rs @@ -1,6 +1,5 @@ -use std::{collections::HashMap, str::FromStr}; - use acropolis_common::{protocol_params::ShelleyParams, Slot, TxHash, TxIdentifier, TxOutRef}; +use std::{collections::HashMap, str::FromStr}; #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct TestContextJson { @@ -10,6 +9,7 @@ pub struct TestContextJson { pub utxos: Vec<((String, u16), (u32, u16))>, } +#[derive(Debug)] pub struct TestContext { pub shelley_params: ShelleyParams, pub current_slot: Slot, diff --git a/modules/tx_unpacker/src/tx_unpacker.rs b/modules/tx_unpacker/src/tx_unpacker.rs index e310ac54..0ce794a2 100644 --- a/modules/tx_unpacker/src/tx_unpacker.rs +++ b/modules/tx_unpacker/src/tx_unpacker.rs @@ -24,6 +24,8 @@ mod state; mod utxo_registry; mod validations; use crate::{state::State, utxo_registry::UTxORegistry}; + +#[cfg(test)] mod test_utils; const DEFAULT_TRANSACTIONS_SUBSCRIBE_TOPIC: &str = "cardano.txs"; diff --git a/modules/tx_unpacker/src/validations/shelley/utxo.rs b/modules/tx_unpacker/src/validations/shelley/utxo.rs index 00593274..a461307b 100644 --- a/modules/tx_unpacker/src/validations/shelley/utxo.rs +++ b/modules/tx_unpacker/src/validations/shelley/utxo.rs @@ -289,20 +289,60 @@ pub fn validate_max_tx_size_utxo( mod tests { use super::*; use crate::{test_utils::TestContext, validation_fixture}; + use acropolis_common::{ShelleyAddress, StakeAddress, TxHash}; use pallas::ledger::traverse; + use std::str::FromStr; use test_case::test_case; #[test_case(validation_fixture!("cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f") => matches Ok(()); + "valid transaction 1" )] #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e") => matches Ok(()); + "valid transaction 2" )] #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "expired_utxo") => matches Err(UTxOValidationError::ExpiredUTxO { ttl: 7084747, current_slot: 7084748 }); + "expired_utxo" )] #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "input_set_empty_utxo") => matches Err(UTxOValidationError::InputSetEmptyUTxO); + "input_set_empty_utxo" + )] + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "fee_too_small_utxo") => + matches Err(UTxOValidationError::FeeTooSmallUTxO { supplied: 22541, required: 172277 }); + "fee_too_small_utxo" + )] + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "bad_inputs_utxo") => + matches Err(UTxOValidationError::BadInputsUTxO { bad_input, bad_input_index }) + if bad_input == TxOutRef::new(TxHash::from_str("d93625fb30376a1eaf90e6232296b0a31b7e63fac2af01381ffe58a574aae537").unwrap(), 1) && bad_input_index == 0; + "bad_inputs_utxo" + )] + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "wrong_network") => + matches Err(UTxOValidationError::WrongNetwork { expected: NetworkId::Mainnet, wrong_address, output_index }) + if wrong_address == Address::Shelley(ShelleyAddress::from_string("addr_test1qzvsy7ftzmrqj3hfs6ppczx263rups3fy3q0z0msnfw2e7s663nkrm3jz3sre0aupn4mdmdz8tdakdhgppaz58qkwe0q680lcj").unwrap()) + && output_index == 1; + "wrong_network" + )] + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "output_too_small_utxo") => + matches Err(UTxOValidationError::OutputTooSmallUTxO { output_index: 1, lovelace: 1, required_lovelace: 1000000 }); + "output_too_small_utxo" + )] + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e", "max_tx_size_utxo") => + matches Err(UTxOValidationError::MaxTxSizeUTxO { supplied: 17983, max: 16384 }); + "max_tx_size_utxo" + )] + /// This tx contains withdrawal + #[test_case(validation_fixture!("a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a") => + matches Ok(()); + "valid transaction 3 with withdrawal" + )] + #[test_case(validation_fixture!("a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a", "wrong_network_withdrawal") => + matches Err(UTxOValidationError::WrongNetworkWithdrawal { expected: NetworkId::Mainnet, wrong_account, withdrawal_index }) + if wrong_account == StakeAddress::from_string("stake_test1upfe3tuzexk65edjy8t4dsfjcs2scyhwwucwkf7qmmg3mmqx3st08").unwrap() + && withdrawal_index == 0; + "wrong_network_withdrawal" )] #[allow(clippy::result_large_err)] fn shelley_test((ctx, raw_tx): (TestContext, Vec)) -> Result<(), UTxOValidationError> { diff --git a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/bad_inputs_utxo.cbor b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/bad_inputs_utxo.cbor new file mode 100644 index 00000000..3fdaddaf --- /dev/null +++ b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/bad_inputs_utxo.cbor @@ -0,0 +1 @@ +84a40081825820d93625fb30376a1eaf90e6232296b0a31b7e63fac2af01381ffe58a574aae53701018282584c82d818584283581ce8bd78295559f7edd927190ec36f7fbe438f74cf0c9380c717aeb56fa101581e581c2b0b011ba3683d490846fd2a91bf44f73a59b1f93e5e753fc1aebed1001a6363d6c21b000000bf32f3680f825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40021a0002a1fd031a006c36c5a1028184582041a0b7a66d454ed0465e7b601d1ff1fe3cddcdf5d127d1da108cbb3555747cd158404c832202e28859cefd3169ef238963a887a396e7773519b68721f9c059abf7bec56422111f4c66b13a116c2d10329a289c815fce6d03f6be903e74c3959bc703582066810329c45d09fd3e396a4eb1ff5e9d4632d5e156b5b320ad992c34d67e25f75822a101581e581c2b0b011ba3683d07b5f91d2ab76a8caab0e4dda6099218e20432e4ccf5f6 \ No newline at end of file diff --git a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/fee_too_small_utxo.cbor b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/fee_too_small_utxo.cbor new file mode 100644 index 00000000..270d8ca9 --- /dev/null +++ b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/fee_too_small_utxo.cbor @@ -0,0 +1 @@ +84a40081825820278ec9a3dfb551288affd8d84b2d6c70b56a153ad1fd43e7b18a5528f687733e01018282584c82d818584283581ce8bd78295559f7edd927190ec36f7fbe438f74cf0c9380c717aeb56fa101581e581c2b0b011ba3683d490846fd2a91bf44f73a59b1f93e5e753fc1aebed1001a6363d6c21b000000bf32f3680f825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c400219580d031a006c36c5a1028184582041a0b7a66d454ed0465e7b601d1ff1fe3cddcdf5d127d1da108cbb3555747cd158404c832202e28859cefd3169ef238963a887a396e7773519b68721f9c059abf7bec56422111f4c66b13a116c2d10329a289c815fce6d03f6be903e74c3959bc703582066810329c45d09fd3e396a4eb1ff5e9d4632d5e156b5b320ad992c34d67e25f75822a101581e581c2b0b011ba3683d07b5f91d2ab76a8caab0e4dda6099218e20432e4ccf5f6 \ No newline at end of file diff --git a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/max_tx_size_utxo.cbor b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/max_tx_size_utxo.cbor new file mode 100644 index 00000000..13363dd0 --- /dev/null +++ b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/max_tx_size_utxo.cbor @@ -0,0 +1 @@ +84a40081825820278ec9a3dfb551288affd8d84b2d6c70b56a153ad1fd43e7b18a5528f687733e010199010182584c82d818584283581ce8bd78295559f7edd927190ec36f7fbe438f74cf0c9380c717aeb56fa101581e581c2b0b011ba3683d490846fd2a91bf44f73a59b1f93e5e753fc1aebed1001a6363d6c21b000000bf32f3680f825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40021a0011e43d031a006c36c5a1028184582041a0b7a66d454ed0465e7b601d1ff1fe3cddcdf5d127d1da108cbb3555747cd158404c832202e28859cefd3169ef238963a887a396e7773519b68721f9c059abf7bec56422111f4c66b13a116c2d10329a289c815fce6d03f6be903e74c3959bc703582066810329c45d09fd3e396a4eb1ff5e9d4632d5e156b5b320ad992c34d67e25f75822a101581e581c2b0b011ba3683d07b5f91d2ab76a8caab0e4dda6099218e20432e4ccf5f6 \ No newline at end of file diff --git a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/output_too_small_utxo.cbor b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/output_too_small_utxo.cbor new file mode 100644 index 00000000..6cf3d2c1 --- /dev/null +++ b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/output_too_small_utxo.cbor @@ -0,0 +1 @@ +84a40081825820278ec9a3dfb551288affd8d84b2d6c70b56a153ad1fd43e7b18a5528f687733e01018282584c82d818584283581ce8bd78295559f7edd927190ec36f7fbe438f74cf0c9380c717aeb56fa101581e581c2b0b011ba3683d490846fd2a91bf44f73a59b1f93e5e753fc1aebed1001a6363d6c21b000000bf32f3680f825839019902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e01021a0002a1fd031a006c36c5a1028184582041a0b7a66d454ed0465e7b601d1ff1fe3cddcdf5d127d1da108cbb3555747cd158404c832202e28859cefd3169ef238963a887a396e7773519b68721f9c059abf7bec56422111f4c66b13a116c2d10329a289c815fce6d03f6be903e74c3959bc703582066810329c45d09fd3e396a4eb1ff5e9d4632d5e156b5b320ad992c34d67e25f75822a101581e581c2b0b011ba3683d07b5f91d2ab76a8caab0e4dda6099218e20432e4ccf5f6 \ No newline at end of file diff --git a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/wrong_network.cbor b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/wrong_network.cbor new file mode 100644 index 00000000..bb0bf133 --- /dev/null +++ b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/wrong_network.cbor @@ -0,0 +1 @@ +84a40081825820278ec9a3dfb551288affd8d84b2d6c70b56a153ad1fd43e7b18a5528f687733e01018282584c82d818584283581ce8bd78295559f7edd927190ec36f7fbe438f74cf0c9380c717aeb56fa101581e581c2b0b011ba3683d490846fd2a91bf44f73a59b1f93e5e753fc1aebed1001a6363d6c21b000000bf32f3680f825839009902792b16c60946e986821c08cad447c0c2292440f13f709a5cacfa1ad46761ee3214603cbfbc0cebb6eda23adbdb36e8087a2a1c16765e1b0000000ba4324c40021a0002a1fd031a006c36c5a1028184582041a0b7a66d454ed0465e7b601d1ff1fe3cddcdf5d127d1da108cbb3555747cd158404c832202e28859cefd3169ef238963a887a396e7773519b68721f9c059abf7bec56422111f4c66b13a116c2d10329a289c815fce6d03f6be903e74c3959bc703582066810329c45d09fd3e396a4eb1ff5e9d4632d5e156b5b320ad992c34d67e25f75822a101581e581c2b0b011ba3683d07b5f91d2ab76a8caab0e4dda6099218e20432e4ccf5f6 \ No newline at end of file diff --git a/modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/context.json b/modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/context.json new file mode 100644 index 00000000..7685bc3e --- /dev/null +++ b/modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/context.json @@ -0,0 +1,59 @@ +{ + "current_slot": 9244417, + "shelley_params": { + "activeSlotsCoeff": 0.05, + "epochLength": 432000, + "maxKesEvolutions": 62, + "maxLovelaceSupply": 45000000000000000, + "networkId": "Mainnet", + "networkMagic": 764824073, + "protocolParams": { + "protocolVersion": { + "minor": 0, + "major": 2 + }, + "maxTxSize": 16384, + "maxBlockBodySize": 65536, + "maxBlockHeaderSize": 1100, + "keyDeposit": 2000000, + "minUTxOValue": 1000000, + "minFeeA": 44, + "minFeeB": 155381, + "poolDeposit": 500000000, + "nOpt": 150, + "minPoolCost": 340000000, + "eMax": 18, + "extraEntropy": { + "tag": "NeutralNonce" + }, + "decentralisationParam": 1, + "rho": 0.003, + "tau": 0.2, + "a0": 0.3 + }, + "securityParam": 2160, + "slotLength": 1, + "slotsPerKesPeriod": 129600, + "systemStart": "2017-09-23T21:44:51Z", + "updateQuorum": 5, + "genDelegs": {} + }, + "utxos": [ + [ + ["0035edcf1ec947ee542330a8dad1f71c426af6958dd863ca4528cbfcff7b0d45", 0], + [4701984, 0] + ], + [ + ["2197beb48981c6bc0eed1f6f47da9910763a4be0450f249a3db5fa8fbaf70c00", 5], + [4491004, 0] + ], + [ + ["2197beb48981c6bc0eed1f6f47da9910763a4be0450f249a3db5fa8fbaf70c00", 11], + [4491004, 0] + ], + [ + ["d9ca0ba5ff5b5096fb7bbc2b967685db5c70c17bb0b5120aec03a0802b3f312d", 0], + [4530419, 0] + ] + ] +} diff --git a/modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/tx.cbor b/modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/tx.cbor new file mode 100644 index 00000000..f8425c58 --- /dev/null +++ b/modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/tx.cbor @@ -0,0 +1 @@ +84a500848258200035edcf1ec947ee542330a8dad1f71c426af6958dd863ca4528cbfcff7b0d45008258202197beb48981c6bc0eed1f6f47da9910763a4be0450f249a3db5fa8fbaf70c00058258202197beb48981c6bc0eed1f6f47da9910763a4be0450f249a3db5fa8fbaf70c000b825820d9ca0ba5ff5b5096fb7bbc2b967685db5c70c17bb0b5120aec03a0802b3f312d000182825839013261ca1f486f7afb6b1983ab768e2ef5e9577306bfea57455989e1c65398af82c9adaa65b221d756c132c4150c12ee7730eb27c0ded11dec1b00000004ce8617058258390151a6fa6547ce05ce28f400414595a0f03062e979008d7f9e55f4756e318a09b650a8e0b01486e093d39b09d16b756cf39fb906e16a642f621b0000001fe5d61a00021a0002f139031a008d2b1905a1581de15398af82c9adaa65b221d756c132c4150c12ee7730eb27c0ded11dec1a12172fd1a10085825820b863a9893cce1d082882e18d2ae3ea7aa65c04b404630069edf25863980a3471584061641a36523eb8168e85633405dce2a70cf938a00d9d2a3d26b5ef8f44eb8fc4dd9fe759537c8d977d369c6db50328c1ee3fedf84abc811f8e6a42d7b1266c0c825820b7804a849bd78e16b0e4288a72f7ed53d8f1f5026a230e7a58b5324fd0ccf4b058403f132126e3f6166d578a55ea9978aeed195ea6317919f84df86f2ba9df8fb14664421a70aa1e011dd5e41374c8ff10b57a3341c68d8674b16b66666f7810030a8258203b76d38d9e2770dc2108408762be264be5ef9f733ae8e5f8073efc5b3b82de385840e546a07eba8cbdf78d75a364f7396baeb0c721f5a3b00fbb3216f3896d053dd169ca2dcde80f665e9cf15619c1a8808534bed84d16846d0b427bc2618700690f825820e78d0b5d0688f8074ad267246e406022c6e6ccd7a73fbb56bcbba3cccaa9a278584060647ed5d67a5cb2d9f51e7fbf95b6bef01699b0ad24a99eb0a882341286aadf36ee5bfcd19aa5918bfff34a39d8158d472d083fce21eb000a115e3732176102825820edc6836e44b94e4c9e01447ed2a55da3fdbc6f59a85b04bc684726bc9ad99d1658402656ad848fd0340be600519044b97fe2227387f19b0f6ebc70de756871b53b64a65c51c8e8bb3f599cdf1378675b6e0506ccc0a6339217684e784750c66e2c02f5f6 \ No newline at end of file diff --git a/modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/wrong_network_withdrawal.cbor b/modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/wrong_network_withdrawal.cbor new file mode 100644 index 00000000..0e7998a8 --- /dev/null +++ b/modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/wrong_network_withdrawal.cbor @@ -0,0 +1 @@ +84a500848258200035edcf1ec947ee542330a8dad1f71c426af6958dd863ca4528cbfcff7b0d45008258202197beb48981c6bc0eed1f6f47da9910763a4be0450f249a3db5fa8fbaf70c00058258202197beb48981c6bc0eed1f6f47da9910763a4be0450f249a3db5fa8fbaf70c000b825820d9ca0ba5ff5b5096fb7bbc2b967685db5c70c17bb0b5120aec03a0802b3f312d000182825839013261ca1f486f7afb6b1983ab768e2ef5e9577306bfea57455989e1c65398af82c9adaa65b221d756c132c4150c12ee7730eb27c0ded11dec1b00000004ce8617058258390151a6fa6547ce05ce28f400414595a0f03062e979008d7f9e55f4756e318a09b650a8e0b01486e093d39b09d16b756cf39fb906e16a642f621b0000001fe5d61a00021a0002f139031a008d2b1905a1581de05398af82c9adaa65b221d756c132c4150c12ee7730eb27c0ded11dec1a12172fd1a10085825820b863a9893cce1d082882e18d2ae3ea7aa65c04b404630069edf25863980a3471584061641a36523eb8168e85633405dce2a70cf938a00d9d2a3d26b5ef8f44eb8fc4dd9fe759537c8d977d369c6db50328c1ee3fedf84abc811f8e6a42d7b1266c0c825820b7804a849bd78e16b0e4288a72f7ed53d8f1f5026a230e7a58b5324fd0ccf4b058403f132126e3f6166d578a55ea9978aeed195ea6317919f84df86f2ba9df8fb14664421a70aa1e011dd5e41374c8ff10b57a3341c68d8674b16b66666f7810030a8258203b76d38d9e2770dc2108408762be264be5ef9f733ae8e5f8073efc5b3b82de385840e546a07eba8cbdf78d75a364f7396baeb0c721f5a3b00fbb3216f3896d053dd169ca2dcde80f665e9cf15619c1a8808534bed84d16846d0b427bc2618700690f825820e78d0b5d0688f8074ad267246e406022c6e6ccd7a73fbb56bcbba3cccaa9a278584060647ed5d67a5cb2d9f51e7fbf95b6bef01699b0ad24a99eb0a882341286aadf36ee5bfcd19aa5918bfff34a39d8158d472d083fce21eb000a115e3732176102825820edc6836e44b94e4c9e01447ed2a55da3fdbc6f59a85b04bc684726bc9ad99d1658402656ad848fd0340be600519044b97fe2227387f19b0f6ebc70de756871b53b64a65c51c8e8bb3f599cdf1378675b6e0506ccc0a6339217684e784750c66e2c02f5f6 \ No newline at end of file