diff --git a/Cargo.lock b/Cargo.lock index 7ffb6fa7f..59bcfff9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -528,6 +528,7 @@ dependencies = [ name = "acropolis_module_utxo_state" version = "0.1.0" dependencies = [ + "acropolis_codec", "acropolis_common", "anyhow", "async-trait", @@ -535,8 +536,13 @@ dependencies = [ "config", "dashmap", "fjall", + "hex", + "pallas 0.33.0", + "serde", "serde_cbor", + "serde_json", "sled", + "test-case", "tokio", "tracing", ] diff --git a/codec/src/tx.rs b/codec/src/tx.rs index f354b683a..1b010b615 100644 --- a/codec/src/tx.rs +++ b/codec/src/tx.rs @@ -7,33 +7,36 @@ use crate::{ }; use acropolis_common::{validation::Phase1ValidationError, *}; use pallas_primitives::Metadatum as PallasMetadatum; -use pallas_traverse::{Era as PallasEra, MultiEraTx}; +use pallas_traverse::{Era as PallasEra, MultiEraInput, MultiEraTx}; -/// Parse transaction inputs and outputs, and return the parsed inputs, outputs, total output lovelace, and errors -pub fn map_transaction_inputs_outputs( +pub fn map_transaction_inputs(inputs: &[MultiEraInput]) -> Vec { + inputs + .iter() + .map(|input| { + let oref = input.output_ref(); + UTxOIdentifier::new(TxHash::from(**oref.hash()), oref.index() as u16) + }) + .collect() +} + +/// Parse transaction consumes and produces, and return the parsed consumes, produces, total output lovelace, and errors +pub fn map_transaction_consumes_produces( tx: &MultiEraTx, ) -> (Vec, Vec, u128, Vec) { - let mut parsed_inputs = Vec::new(); - let mut parsed_outputs = Vec::new(); + let parsed_consumes = map_transaction_inputs(&tx.consumes()); + let mut parsed_produces = Vec::new(); let mut total_output = 0; let mut errors = Vec::new(); let tx_hash = TxHash::from(*tx.hash()); - for input in tx.consumes() { - let oref = input.output_ref(); - let utxo = UTxOIdentifier::new(TxHash::from(**oref.hash()), oref.index() as u16); - - parsed_inputs.push(utxo); - } - - for (index, output) in tx.outputs().iter().enumerate() { + for (index, output) in tx.produces() { let utxo = UTxOIdentifier::new(tx_hash, index as u16); match output.address() { Ok(pallas_address) => match map_address(&pallas_address) { Ok(address) => { // Add TxOutput to utxo_deltas - parsed_outputs.push(TxOutput { + parsed_produces.push(TxOutput { utxo_identifier: utxo, address, value: map_value(&output.value()), @@ -52,7 +55,7 @@ pub fn map_transaction_inputs_outputs( } } - (parsed_inputs, parsed_outputs, total_output, errors) + (parsed_consumes, parsed_produces, total_output, errors) } pub fn map_metadata(metadata: &PallasMetadatum) -> Metadata { @@ -75,7 +78,8 @@ pub fn map_transaction( network_id: NetworkId, era: Era, ) -> Transaction { - let (inputs, outputs, total_output, input_output_errors) = map_transaction_inputs_outputs(tx); + let (consumes, produces, total_output, input_output_errors) = + map_transaction_consumes_produces(tx); let mut errors = input_output_errors; let mut certs = Vec::new(); @@ -141,8 +145,8 @@ pub fn map_transaction( errors.extend(vkey_witness_errors); Transaction { - inputs, - outputs, + consumes, + produces, total_output, certs, withdrawals, diff --git a/common/src/tx.rs b/common/src/tx.rs index 1cc83258e..ab6235687 100644 --- a/common/src/tx.rs +++ b/common/src/tx.rs @@ -4,8 +4,8 @@ use crate::{ }; pub struct Transaction { - pub inputs: Vec, - pub outputs: Vec, + pub consumes: Vec, + pub produces: Vec, pub total_output: u128, pub certs: Vec, pub withdrawals: Vec, diff --git a/common/src/types.rs b/common/src/types.rs index 5fba1535d..64ec2c251 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -30,6 +30,7 @@ use std::{ ops::{AddAssign, Neg}, str::FromStr, }; +use tracing::error; /// Network identifier #[derive( @@ -288,8 +289,8 @@ pub struct TxUTxODeltas { pub tx_identifier: TxIdentifier, // Created and spent UTxOs - pub inputs: Vec, - pub outputs: Vec, + pub consumes: Vec, + pub produces: Vec, // State needed for validation // This is missing UTxO Authors @@ -890,6 +891,17 @@ pub struct TxOutput { pub reference_script: Option, } +impl TxOutput { + pub fn utxo_value(&self) -> UTXOValue { + UTXOValue { + address: self.address.clone(), + value: self.value.clone(), + datum: self.datum.clone(), + reference_script: self.reference_script.clone(), + } + } +} + /// Key hash pub type KeyHash = Hash<28>; @@ -1149,6 +1161,22 @@ pub struct Withdrawal { pub tx_identifier: TxIdentifier, } +impl Withdrawal { + pub fn get_withdrawal_authors( + &self, + vkey_hashes: &mut HashSet, + script_hashes: &mut HashSet, + ) { + match self.address.credential { + StakeCredential::AddrKeyHash(vkey_hash) => { + vkey_hashes.insert(vkey_hash); + } + StakeCredential::ScriptHash(script_hash) => { + script_hashes.insert(script_hash); + } + } + } +} /// Treasury pot account #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum Pot { @@ -1880,6 +1908,12 @@ impl AsRef> for GenesisDelegates { } } +impl From> for GenesisDelegates { + fn from(map: HashMap) -> Self { + GenesisDelegates(map.into_iter().map(|(k, v)| (*k, v)).collect()) + } +} + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct ProtocolConsts { pub k: usize, @@ -2081,6 +2115,23 @@ pub struct AlonzoBabbageUpdateProposal { pub enactment_epoch: u64, } +impl AlonzoBabbageUpdateProposal { + pub fn get_governance_authors( + &self, + vkey_hashes: &mut HashSet, + genesis_delegs: &GenesisDelegates, + ) { + for (genesis_key, _) in self.proposals.iter() { + let found_genesis = genesis_delegs.as_ref().get(genesis_key); + if let Some(genesis) = found_genesis { + vkey_hashes.insert(genesis.delegate); + } else { + error!("Genesis delegate not found: {genesis_key}"); + } + } + } +} + #[derive(Serialize, PartialEq, Eq, Deserialize, Debug, Clone)] pub struct Constitution { pub anchor: Anchor, @@ -2518,10 +2569,11 @@ impl TxCertificate { /// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/TxCert.hs#L583 /// /// returns (vkey_hashes, script_hashes) - pub fn get_cert_authors(&self) -> (HashSet, HashSet) { - let mut vkey_hashes = HashSet::new(); - let mut script_hashes = HashSet::new(); - + pub fn get_cert_authors( + &self, + vkey_hashes: &mut HashSet, + script_hashes: &mut HashSet, + ) { let mut parse_cred = |cred: &StakeCredential| match cred { StakeCredential::AddrKeyHash(vkey_hash) => { vkey_hashes.insert(*vkey_hash); @@ -2557,8 +2609,6 @@ impl TxCertificate { } _ => {} } - - (vkey_hashes, script_hashes) } } diff --git a/common/src/validation.rs b/common/src/validation.rs index e1ce338cb..8cca394c0 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -61,7 +61,7 @@ pub enum ValidationError { BadKES(#[from] KesValidationError), #[error( - "Invalid Transactions: {}", + "bad_transactions: {}", bad_transactions .iter() .map(|(tx_index, error)| format!("tx-index={tx_index}, error={error}")) @@ -72,11 +72,15 @@ pub enum ValidationError { bad_transactions: Vec<(u16, TransactionValidationError)>, }, - #[error("CBOR Decoding error")] - CborDecodeError(usize, String), - #[error("Governance failure: {0}")] BadGovernance(#[from] GovernanceValidationError), + + #[error("CBOR Decoding error")] + CborDecodeError { + era: Era, + slot: Slot, + reason: String, + }, } /// Transaction Validation Error @@ -195,10 +199,6 @@ pub enum UTxOValidationError { required_lovelace: Lovelace, }, - /// **Cause:** The transaction size is too big. - #[error("Max tx size: supplied={supplied}, max={max}")] - MaxTxSizeUTxO { supplied: u32, max: u32 }, - /// **Cause:** Malformed UTxO #[error("Malformed UTxO: era={era}, reason={reason}")] MalformedUTxO { era: Era, reason: String }, @@ -285,37 +285,46 @@ pub enum VrfValidationError { /// **Cause:** Block issuer's pool ID is not registered in current stake distribution #[error("Unknown Pool: {}", hex::encode(pool_id))] UnknownPool { pool_id: PoolId }, + /// **Cause:** The VRF key hash in the block header doesn't match the VRF key /// registered with this stake pool in the ledger state for Overlay slot #[error("{0}")] WrongGenesisLeaderVrfKey(#[from] WrongGenesisLeaderVrfKeyError), + /// **Cause:** The VRF key hash in the block header doesn't match the VRF key /// registered with this stake pool in the ledger state #[error("{0}")] WrongLeaderVrfKey(#[from] WrongLeaderVrfKeyError), + /// VRF nonce proof verification failed (TPraos rho - nonce proof) /// **Cause:** The (rho - nonce) VRF proof failed verification #[error("{0}")] TPraosBadNonceVrfProof(#[from] TPraosBadNonceVrfProofError), + /// VRF leader proof verification failed (TPraos y - leader proof) /// **Cause:** The (y - leader) VRF proof failed verification #[error("{0}")] TPraosBadLeaderVrfProof(#[from] TPraosBadLeaderVrfProofError), + /// VRF proof cryptographic verification failed (Praos single proof) /// **Cause:** The cryptographic VRF proof is invalid #[error("{0}")] PraosBadVrfProof(#[from] PraosBadVrfProofError), + /// **Cause:** The VRF output is too large for this pool's stake. /// The pool lost the slot lottery #[error("{0}")] VrfLeaderValueTooBig(#[from] VrfLeaderValueTooBigError), + /// **Cause:** This slot is in the overlay schedule but marked as non-active. /// It's an intentional gap slot where no blocks should be produced. #[error("Not Active slot in overlay schedule: {slot}")] NotActiveSlotInOverlaySchedule { slot: Slot }, + /// **Cause:** Some data has incorrect bytes #[error("TryFromSlice: {0}")] TryFromSlice(String), + /// **Cause:** Other errors (e.g. Invalid shelley params, praos params, missing data) #[error("{0}")] Other(String), diff --git a/modules/assets_state/src/state.rs b/modules/assets_state/src/state.rs index b809d1293..8abe53b5e 100644 --- a/modules/assets_state/src/state.rs +++ b/modules/assets_state/src/state.rs @@ -412,7 +412,7 @@ impl State { for tx in deltas { let mut tx_asset_ids = HashSet::new(); - for output in &tx.outputs { + for output in &tx.produces { for (policy_id, assets) in &output.value.assets { for asset in assets { if let Some(asset_id) = registry.lookup_id(policy_id, &asset.name) { @@ -589,7 +589,7 @@ impl State { let mut new_info = self.info.clone(); for tx in deltas { - for output in &tx.outputs { + for output in &tx.produces { let Some(Datum::Inline(blob)) = &output.datum else { continue; }; @@ -864,13 +864,13 @@ mod tests { fn make_tx_utxo_deltas( tx_identifier: TxIdentifier, - inputs: Vec, - outputs: Vec, + consumes: Vec, + produces: Vec, ) -> TxUTxODeltas { TxUTxODeltas { tx_identifier, - inputs, - outputs, + consumes, + produces, vkey_hashes_needed: None, script_hashes_needed: None, vkey_hashes_provided: None, diff --git a/modules/block_kes_validator/src/block_kes_validator.rs b/modules/block_kes_validator/src/block_kes_validator.rs index 9d8c687f2..5a67afc09 100644 --- a/modules/block_kes_validator/src/block_kes_validator.rs +++ b/modules/block_kes_validator/src/block_kes_validator.rs @@ -5,6 +5,7 @@ use acropolis_common::{ caryatid::SubscriptionExt, messages::{CardanoMessage, Message}, state_history::{StateHistory, StateHistoryStore}, + validation::ValidationOutcomes, BlockInfo, BlockStatus, }; use anyhow::Result; @@ -15,9 +16,6 @@ use tokio::sync::Mutex; use tracing::{error, info, info_span, Instrument}; mod state; use state::State; - -use crate::kes_validation_publisher::KesValidationPublisher; -mod kes_validation_publisher; mod ouroboros; const DEFAULT_VALIDATION_KES_PUBLISHER_TOPIC: (&str, &str) = @@ -48,12 +46,13 @@ pub struct BlockKesValidator; impl BlockKesValidator { #[allow(clippy::too_many_arguments)] async fn run( + context: Arc>, history: Arc>>, - kes_validation_publisher: KesValidationPublisher, mut bootstrapped_subscription: Box>, mut block_subscription: Box>, mut protocol_parameters_subscription: Box>, mut spo_state_subscription: Box>, + kes_validation_publisher_topic: String, ) -> Result<()> { let (_, bootstrapped_message) = bootstrapped_subscription.read().await?; let genesis = match bootstrapped_message.as_ref() { @@ -115,24 +114,27 @@ impl BlockKesValidator { let span = info_span!("block_kes_validator.validate", block = block_info.number); async { - let result = state - .validate_block_kes(block_info, &block_msg.header, &genesis) - .map_err(|e| *e); - - // Update the operational certificate counter - // When block is validated successfully - // Reference - // https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L508 - if let Ok(Some((pool_id, updated_sequence_number))) = result.as_ref() { - state.update_ocert_counter(*pool_id, *updated_sequence_number); + let mut validation_outcomes = ValidationOutcomes::new(); + let result = + state.validate(block_info, &block_msg.header, &genesis).map_err(|e| *e); + match result { + Ok(Some((pool_id, updated_sequence_number))) => { + // Update the operational certificate counter + // When block is validated successfully + // Reference + // https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L508 + state.update_ocert_counter(pool_id, updated_sequence_number); + } + Err(e) => { + validation_outcomes.push(e); + } + _ => {} } - if let Err(e) = kes_validation_publisher - .publish_kes_validation(block_info, result) + validation_outcomes + .publish(&context, &kes_validation_publisher_topic, block_info) .await - { - error!("Failed to publish KES validation: {e}") - } + .unwrap_or_else(|e| error!("Failed to publish KES validation: {e}")); } .instrument(span) .await; @@ -175,10 +177,6 @@ impl BlockKesValidator { .unwrap_or(DEFAULT_SPO_STATE_SUBSCRIBE_TOPIC.1.to_string()); info!("Creating spo state subscription on '{spo_state_subscribe_topic}'"); - // publishers - let kes_validation_publisher = - KesValidationPublisher::new(context.clone(), validation_kes_publisher_topic); - // Subscribers let bootstrapped_subscription = context.subscribe(&bootstrapped_subscribe_topic).await?; let block_subscription = context.subscribe(&block_subscribe_topic).await?; @@ -193,14 +191,16 @@ impl BlockKesValidator { ))); // Start run task + let context_run = context.clone(); context.run(async move { Self::run( + context_run, history, - kes_validation_publisher, bootstrapped_subscription, block_subscription, protocol_parameters_subscription, spo_state_subscription, + validation_kes_publisher_topic, ) .await .unwrap_or_else(|e| error!("Failed: {e}")); diff --git a/modules/block_kes_validator/src/kes_validation_publisher.rs b/modules/block_kes_validator/src/kes_validation_publisher.rs deleted file mode 100644 index fe9254ea6..000000000 --- a/modules/block_kes_validator/src/kes_validation_publisher.rs +++ /dev/null @@ -1,52 +0,0 @@ -use acropolis_common::{ - messages::{CardanoMessage, Message}, - validation::{KesValidationError, ValidationError, ValidationStatus}, - BlockInfo, PoolId, -}; -use caryatid_sdk::Context; -use std::sync::Arc; -use tracing::error; - -/// Message publisher for Block header KES Validation Result -pub struct KesValidationPublisher { - /// Module context - context: Arc>, - - /// Topic to publish on - topic: String, -} - -impl KesValidationPublisher { - /// Construct with context and topic to publish on - pub fn new(context: Arc>, topic: String) -> Self { - Self { context, topic } - } - - pub async fn publish_kes_validation( - &self, - block: &BlockInfo, - validation_result: Result, KesValidationError>, - ) -> anyhow::Result<()> { - let validation_status = match validation_result { - Ok(_) => ValidationStatus::Go, - Err(error) => { - error!( - "KES validation failed: {} of block {}", - error.clone(), - block.number - ); - ValidationStatus::NoGo(ValidationError::from(error)) - } - }; - self.context - .message_bus - .publish( - &self.topic, - Arc::new(Message::Cardano(( - block.clone(), - CardanoMessage::BlockValidation(validation_status), - ))), - ) - .await - } -} diff --git a/modules/block_kes_validator/src/state.rs b/modules/block_kes_validator/src/state.rs index 6eb8fc6a9..9b42a8286 100644 --- a/modules/block_kes_validator/src/state.rs +++ b/modules/block_kes_validator/src/state.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; use acropolis_common::{ genesis_values::GenesisValues, messages::{ProtocolParamsMessage, SPOStateMessage}, - validation::KesValidationError, + validation::{KesValidationError, ValidationError}, BlockInfo, PoolId, }; use imbl::HashMap; @@ -49,12 +49,12 @@ impl State { self.ocert_counters.insert(pool_id, declared_sequence_number); } - pub fn validate_block_kes( + pub fn validate( &self, block_info: &BlockInfo, raw_header: &[u8], genesis: &GenesisValues, - ) -> Result, Box> { + ) -> Result, Box> { // Validation starts after Shelley Era if block_info.epoch < genesis.shelley_epoch { return Ok(None); @@ -64,22 +64,23 @@ impl State { Ok(header) => header, Err(e) => { error!("Can't decode header {}: {e}", block_info.slot); - return Err(Box::new(KesValidationError::Other(format!( - "Can't decode header {}: {e}", - block_info.slot - )))); + return Err(Box::new(ValidationError::CborDecodeError { + era: block_info.era, + slot: block_info.slot, + reason: e.to_string(), + })); } }; let Some(slots_per_kes_period) = self.slots_per_kes_period else { - return Err(Box::new(KesValidationError::Other( - "Slots per KES period is not set".to_string(), - ))); + return Err(Box::new( + KesValidationError::Other("Slots per KES period is not set".to_string()).into(), + )); }; let Some(max_kes_evolutions) = self.max_kes_evolutions else { - return Err(Box::new(KesValidationError::Other( - "Max KES evolutions is not set".to_string(), - ))); + return Err(Box::new( + KesValidationError::Other("Max KES evolutions is not set".to_string()).into(), + )); }; let result = ouroboros::kes_validation::validate_block_kes( @@ -95,6 +96,6 @@ impl State { Ok(Some((pool_id, declared_sequence_number))) }); - result + result.map_err(|e| Box::new((*e).into())) } } diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs index 95f27fbd1..bb5e2e744 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -5,6 +5,7 @@ use acropolis_common::{ caryatid::SubscriptionExt, messages::{CardanoMessage, Message}, state_history::{StateHistory, StateHistoryStore}, + validation::ValidationOutcomes, BlockInfo, BlockStatus, }; use anyhow::Result; @@ -17,9 +18,7 @@ mod state; use state::State; mod ouroboros; -use crate::vrf_validation_publisher::VrfValidationPublisher; mod snapshot; -mod vrf_validation_publisher; const DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC: (&str, &str) = ("validation-vrf-publisher-topic", "cardano.validation.vrf"); @@ -53,14 +52,15 @@ pub struct BlockVrfValidator; impl BlockVrfValidator { #[allow(clippy::too_many_arguments)] async fn run( + context: Arc>, history: Arc>>, - mut vrf_validation_publisher: VrfValidationPublisher, mut bootstrapped_subscription: Box>, mut block_subscription: Box>, mut protocol_parameters_subscription: Box>, mut epoch_nonce_subscription: Box>, mut spo_state_subscription: Box>, mut spdd_subscription: Box>, + publish_vrf_validation_topic: String, ) -> Result<()> { let (_, bootstrapped_message) = bootstrapped_subscription.read().await?; let genesis = match bootstrapped_message.as_ref() { @@ -153,15 +153,15 @@ impl BlockVrfValidator { let span = info_span!("block_vrf_validator.validate", block = block_info.number); async { - let result = state - .validate_block_vrf(block_info, &block_msg.header, &genesis) - .map_err(|e| *e); - if let Err(e) = vrf_validation_publisher - .publish_vrf_validation(block_info, result) - .await - { - error!("Failed to publish VRF validation: {e}") + let mut validation_outcomes = ValidationOutcomes::new(); + if let Err(e) = state.validate(block_info, &block_msg.header, &genesis) { + validation_outcomes.push(*e); } + + validation_outcomes + .publish(&context, &publish_vrf_validation_topic, block_info) + .await + .unwrap_or_else(|e| error!("Failed to publish VRF validation: {e}")); } .instrument(span) .await; @@ -213,10 +213,6 @@ impl BlockVrfValidator { .unwrap_or(DEFAULT_SPDD_SUBSCRIBE_TOPIC.1.to_string()); info!("Creating spdd subscription on '{spdd_subscribe_topic}'"); - // publishers - let vrf_validation_publisher = - VrfValidationPublisher::new(context.clone(), validation_vrf_publisher_topic); - // Subscribers let bootstrapped_subscription = context.subscribe(&bootstrapped_subscribe_topic).await?; let protocol_parameters_subscription = @@ -233,16 +229,18 @@ impl BlockVrfValidator { ))); // Start run task + let context_run = context.clone(); context.run(async move { Self::run( + context_run, history, - vrf_validation_publisher, bootstrapped_subscription, block_subscription, protocol_parameters_subscription, epoch_nonce_subscription, spo_state_subscription, spdd_subscription, + validation_vrf_publisher_topic, ) .await .unwrap_or_else(|e| error!("Failed: {e}")); diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index 5efbca0ed..dbd879fb9 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -8,7 +8,7 @@ use acropolis_common::{ messages::{ProtocolParamsMessage, SPOStakeDistributionMessage, SPOStateMessage}, protocol_params::Nonce, rational_number::RationalNumber, - validation::VrfValidationError, + validation::{ValidationError, VrfValidationError}, BlockInfo, Era, }; use anyhow::Result; @@ -74,12 +74,12 @@ impl State { self.epoch_snapshots.push(new_snapshot); } - pub fn validate_block_vrf( + pub fn validate( &self, block_info: &BlockInfo, raw_header: &[u8], genesis: &GenesisValues, - ) -> Result<(), Box> { + ) -> Result<(), Box> { // Validation starts after Shelley Era if block_info.epoch < genesis.shelley_epoch { return Ok(()); @@ -89,27 +89,28 @@ impl State { Ok(header) => header, Err(e) => { error!("Can't decode header {}: {e}", block_info.slot); - return Err(Box::new(VrfValidationError::Other(format!( - "Can't decode header {}: {e}", - block_info.slot - )))); + return Err(Box::new(ValidationError::CborDecodeError { + era: block_info.era, + slot: block_info.slot, + reason: e.to_string(), + })); } }; let Some(decentralisation_param) = self.decentralisation_param.clone() else { - return Err(Box::new(VrfValidationError::Other( - "Decentralisation Param is not set".to_string(), - ))); + return Err(Box::new( + VrfValidationError::Other("Decentralisation Param is not set".to_string()).into(), + )); }; let Some(active_slots_coeff) = self.active_slots_coeff.clone() else { - return Err(Box::new(VrfValidationError::Other( - "Active Slots Coeff is not set".to_string(), - ))); + return Err(Box::new( + VrfValidationError::Other("Active Slots Coeff is not set".to_string()).into(), + )); }; let Some(epoch_nonce) = self.epoch_nonce.as_ref() else { - return Err(Box::new(VrfValidationError::Other( - "Epoch Nonce is not set".to_string(), - ))); + return Err(Box::new( + VrfValidationError::Other("Epoch Nonce is not set".to_string()).into(), + )); }; let is_tpraos = matches!( @@ -117,8 +118,8 @@ impl State { Era::Shelley | Era::Allegra | Era::Mary | Era::Alonzo ); - if is_tpraos { - let result = ouroboros::tpraos::validate_vrf_tpraos( + let result = if is_tpraos { + ouroboros::tpraos::validate_vrf_tpraos( block_info, &header, epoch_nonce, @@ -131,10 +132,9 @@ impl State { ) .and_then(|vrf_validations| { vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) - }); - result + }) } else { - let result = ouroboros::praos::validate_vrf_praos( + ouroboros::praos::validate_vrf_praos( block_info, &header, epoch_nonce, @@ -145,8 +145,8 @@ impl State { ) .and_then(|vrf_validations| { vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) - }); - result - } + }) + }; + result.map_err(|e| Box::new((*e).into())) } } diff --git a/modules/block_vrf_validator/src/vrf_validation_publisher.rs b/modules/block_vrf_validator/src/vrf_validation_publisher.rs deleted file mode 100644 index 7dcbba3cd..000000000 --- a/modules/block_vrf_validator/src/vrf_validation_publisher.rs +++ /dev/null @@ -1,52 +0,0 @@ -use acropolis_common::{ - messages::{CardanoMessage, Message}, - validation::{ValidationError, ValidationStatus, VrfValidationError}, - BlockInfo, -}; -use caryatid_sdk::Context; -use std::sync::Arc; -use tracing::error; - -/// Message publisher for Block header Vrf Validation Result -pub struct VrfValidationPublisher { - /// Module context - context: Arc>, - - /// Topic to publish on - topic: String, -} - -impl VrfValidationPublisher { - /// Construct with context and topic to publish on - pub fn new(context: Arc>, topic: String) -> Self { - Self { context, topic } - } - - pub async fn publish_vrf_validation( - &mut self, - block: &BlockInfo, - validation_result: Result<(), VrfValidationError>, - ) -> anyhow::Result<()> { - let validation_status = match validation_result { - Ok(_) => ValidationStatus::Go, - Err(error) => { - error!( - "VRF validation failed: {} of block {}", - error.clone(), - block.number - ); - ValidationStatus::NoGo(ValidationError::from(error)) - } - }; - self.context - .message_bus - .publish( - &self.topic, - Arc::new(Message::Cardano(( - block.clone(), - CardanoMessage::BlockValidation(validation_status), - ))), - ) - .await - } -} diff --git a/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs b/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs index d26d86e9e..be4d2e30c 100644 --- a/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs +++ b/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs @@ -173,8 +173,8 @@ impl GenesisBootstrapper { utxo_deltas_message.deltas.push(TxUTxODeltas { tx_identifier, - inputs: Vec::new(), - outputs: vec![tx_output], + consumes: Vec::new(), + produces: vec![tx_output], vkey_hashes_needed: None, script_hashes_needed: None, vkey_hashes_provided: None, diff --git a/modules/tx_unpacker/src/state.rs b/modules/tx_unpacker/src/state.rs index ce13d17bd..5cffb0896 100644 --- a/modules/tx_unpacker/src/state.rs +++ b/modules/tx_unpacker/src/state.rs @@ -1,7 +1,9 @@ use crate::validations; use acropolis_common::{ - messages::ProtocolParamsMessage, protocol_params::ProtocolParams, - validation::TransactionValidationError, BlockInfo, Era, GenesisDelegates, + messages::{ProtocolParamsMessage, RawTxsMessage}, + protocol_params::ProtocolParams, + validation::{TransactionValidationError, ValidationError}, + BlockInfo, Era, GenesisDelegates, }; use anyhow::Result; @@ -21,7 +23,7 @@ impl State { self.protocol_params = msg.params.clone(); } - pub fn validate_transaction( + fn validate_transaction( &self, block_info: &BlockInfo, raw_tx: &[u8], @@ -44,4 +46,29 @@ impl State { _ => Ok(()), } } + + pub fn validate( + &self, + block_info: &BlockInfo, + txs_msg: &RawTxsMessage, + genesis_delegs: &GenesisDelegates, + ) -> Result<(), Box> { + let mut bad_transactions = Vec::new(); + for (tx_index, raw_tx) in txs_msg.txs.iter().enumerate() { + let tx_index = tx_index as u16; + + // Validate transaction + if let Err(e) = self.validate_transaction(block_info, raw_tx, genesis_delegs) { + bad_transactions.push((tx_index, *e)); + } + } + + if bad_transactions.is_empty() { + Ok(()) + } else { + Err(Box::new(ValidationError::BadTransactions { + bad_transactions, + })) + } + } } diff --git a/modules/tx_unpacker/src/tx_unpacker.rs b/modules/tx_unpacker/src/tx_unpacker.rs index 562a69450..abd6eb7b7 100644 --- a/modules/tx_unpacker/src/tx_unpacker.rs +++ b/modules/tx_unpacker/src/tx_unpacker.rs @@ -9,6 +9,7 @@ use acropolis_common::{ StateTransitionMessage, TxCertificatesMessage, UTXODeltasMessage, WithdrawalsMessage, }, state_history::{StateHistory, StateHistoryStore}, + validation::ValidationOutcomes, *, }; use anyhow::Result; @@ -18,14 +19,13 @@ use futures::future::join_all; use pallas::codec::minicbor::encode; use pallas::ledger::traverse::MultiEraTx; use tokio::sync::Mutex; -use tracing::{debug, error, info, info_span}; +use tracing::{debug, error, info, info_span, Instrument}; use crate::state::State; +mod crypto; mod state; -mod tx_validation_publisher; +mod utils; mod validations; -use tx_validation_publisher::TxValidationPublisher; -mod crypto; #[cfg(test)] mod test_utils; @@ -65,7 +65,7 @@ impl TxUnpacker { publish_certificates_topic: Option, publish_governance_procedures_topic: Option, publish_block_txs_topic: Option, - tx_validation_publisher: Option, + publish_tx_validation_topic: Option, // subscribers mut txs_sub: Box>, mut bootstrapped_sub: Box>, @@ -137,8 +137,8 @@ impl TxUnpacker { let tx_identifier = TxIdentifier::new(block_number, tx_index); let Transaction { - inputs: tx_inputs, - outputs: tx_outputs, + consumes: tx_consumes, + produces: tx_produces, total_output: tx_total_output, certs: tx_certs, withdrawals: tx_withdrawals, @@ -150,27 +150,25 @@ impl TxUnpacker { let mut props = None; let mut votes = None; - let (vkey_hashes_needed, script_hashes_needed, vkey_hashes_provided, script_hashes_provided) = if block.intent.do_validation() { - let (vkey_hashes_needed, script_hashes_needed) = Self::get_vkey_script_needed( - &tx_certs, - &tx_withdrawals, - &tx_proposal_update, - ); - let (vkey_hashes_provided, script_hashes_provided) = Self::get_vkey_script_provided( - &vkey_witnesses, - &native_scripts, - ); - (Some(vkey_hashes_needed), Some(script_hashes_needed), Some(vkey_hashes_provided), Some(script_hashes_provided)) - } else { - (None, None, None, None) - }; + let mut vkey_needed = HashSet::new(); + let mut script_needed = HashSet::new(); + utils::get_vkey_script_needed( + &tx_certs, + &tx_withdrawals, + &tx_proposal_update, + &state.protocol_params, + &mut vkey_needed, + &mut script_needed, + ); + let vkey_hashes_provided = vkey_witnesses.iter().map(|w| w.key_hash()).collect::>(); + let script_hashes_provided = native_scripts.iter().map(|s| s.compute_hash()).collect::>(); // sum up total output lovelace for a block total_output += tx_total_output; if tracing::enabled!(tracing::Level::DEBUG) { debug!("Decoded tx with inputs={}, outputs={}, certs={}, total_output={}", - tx_inputs.len(), tx_outputs.len(), tx_certs.len(), tx_total_output); + tx_consumes.len(), tx_produces.len(), tx_certs.len(), tx_total_output); } if let Some(error) = tx_error { @@ -183,12 +181,12 @@ impl TxUnpacker { // Group deltas by tx utxo_deltas.push(TxUTxODeltas { tx_identifier, - inputs: tx_inputs, - outputs: tx_outputs, - vkey_hashes_needed, - script_hashes_needed, - vkey_hashes_provided, - script_hashes_provided, + consumes: tx_consumes, + produces: tx_produces, + vkey_hashes_needed: Some(vkey_needed), + script_hashes_needed: Some(script_needed), + vkey_hashes_provided: Some(vkey_hashes_provided), + script_hashes_provided: Some(script_hashes_provided), }); } @@ -420,25 +418,24 @@ impl TxUnpacker { } } - if let Some(tx_validation_publisher) = tx_validation_publisher.as_ref() { + if let Some(publish_tx_validation_topic) = publish_tx_validation_topic.as_ref() { if let Message::Cardano((block, CardanoMessage::ReceivedTxs(txs_msg))) = message.as_ref() { - let mut tx_errors = Vec::new(); - 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, &genesis.genesis_delegs) - { - tx_errors.push((tx_index, *e)); + let span = info_span!("tx_unpacker.validate", block = block.number); + async { + let mut validation_outcomes = ValidationOutcomes::new(); + if let Err(e) = state.validate(block, txs_msg, &genesis.genesis_delegs) { + validation_outcomes.push(*e); } + + validation_outcomes + .publish(&context, publish_tx_validation_topic, block) + .await + .unwrap_or_else(|e| error!("Failed to publish tx validation: {e}")); } - tx_validation_publisher - .publish_tx_validation(block, tx_errors) - .await - .unwrap_or_else(|e| error!("Failed to publish tx validation: {e}")); + .instrument(span) + .await; } } @@ -484,12 +481,6 @@ impl TxUnpacker { } let publish_tx_validation_topic = config.get_string("publish-tx-validation-topic").ok(); - let tx_validation_publisher = if let Some(ref topic) = publish_tx_validation_topic { - info!("Publishing tx validation on '{topic}'"); - Some(TxValidationPublisher::new(context.clone(), topic.clone())) - } else { - None - }; // Subscribers let transactions_subscribe_topic = config @@ -532,7 +523,7 @@ impl TxUnpacker { publish_certificates_topic, publish_governance_procedures_topic, publish_block_txs_topic, - tx_validation_publisher, + publish_tx_validation_topic, txs_sub, bootstrapped_sub, protocol_params_sub, @@ -544,82 +535,6 @@ impl TxUnpacker { Ok(()) } - /// Get VKey Witnesses needed for transaction - /// Get Scripts needed for transaction - /// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/UTxO.hs#L274 - /// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/UTxO.hs#L226 - /// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/UTxO.hs#L103 - /// - /// VKey Witnesses needed - /// 1. UTxO authors: keys that own the UTxO being spent - /// 2. Certificate authors: keys authorizing certificates - /// 3. Pool owners: owners that must sign pool registration - /// 4. Withdrawal authors: keys authorizing reward withdrawals - /// 5. Governance authors: keys authorizing governance actions (e.g. protocol update) - /// - /// Script Witnesses needed - /// 1. Input scripts: scripts locking UTxO being spent - /// 2. Withdrawal scripts: scripts controlling reward accounts - /// 3. Certificate scripts: scripts in certificate credentials. - /// - /// NOTE: - /// This doesn't count `inputs` - /// which will be considered in the utxos_state - fn get_vkey_script_needed( - certs: &[TxCertificateWithPos], - withdrawals: &[Withdrawal], - proposal_update: &Option, - ) -> (HashSet, HashSet) { - let mut vkey_hashes = HashSet::new(); - let mut script_hashes = HashSet::new(); - - // for each certificate, get the required vkey and script hashes - for cert_with_pos in certs.iter() { - let (v, s) = cert_with_pos.cert.get_cert_authors(); - vkey_hashes.extend(v); - script_hashes.extend(s); - } - - // for each withdrawal, get the required vkey and script hashes - for withdrawal in withdrawals.iter() { - match withdrawal.address.credential { - StakeCredential::AddrKeyHash(vkey_hash) => { - vkey_hashes.insert(vkey_hash); - } - StakeCredential::ScriptHash(script_hash) => { - script_hashes.insert(script_hash); - } - } - } - - // for each governance action, get the required vkey hashes - if let Some(proposal_update) = proposal_update.as_ref() { - for (genesis_key, _) in proposal_update.proposals.iter() { - vkey_hashes.insert(*genesis_key); - } - } - - (vkey_hashes, script_hashes) - } - - fn get_vkey_script_provided( - vkey_witnesses: &[VKeyWitness], - native_scripts: &[NativeScript], - ) -> (Vec, Vec) { - let mut vkey_hashes = Vec::new(); - let mut script_hashes = Vec::new(); - - for vkey_witness in vkey_witnesses.iter() { - vkey_hashes.push(vkey_witness.key_hash()); - } - - for native_script in native_scripts.iter() { - script_hashes.push(native_script.compute_hash()); - } - - (vkey_hashes, script_hashes) - } - /// Check for synchronisation fn check_sync(expected: &Option, actual: &BlockInfo) { if let Some(ref block) = expected { diff --git a/modules/tx_unpacker/src/tx_validation_publisher.rs b/modules/tx_unpacker/src/tx_validation_publisher.rs deleted file mode 100644 index db5a0fc96..000000000 --- a/modules/tx_unpacker/src/tx_validation_publisher.rs +++ /dev/null @@ -1,57 +0,0 @@ -use acropolis_common::{ - messages::{CardanoMessage, Message}, - validation::{TransactionValidationError, ValidationError, ValidationStatus}, - BlockInfo, -}; -use caryatid_sdk::Context; -use std::sync::Arc; -use tracing::error; - -/// Message publisher for Block header Tx Validation Result -pub struct TxValidationPublisher { - /// Module context - context: Arc>, - - /// Topic to publish on - topic: String, -} - -impl TxValidationPublisher { - /// Construct with context and topic to publish on - pub fn new(context: Arc>, topic: String) -> Self { - Self { context, topic } - } - - pub async fn publish_tx_validation( - &self, - block: &BlockInfo, - tx_errors: Vec<(u16, TransactionValidationError)>, - ) -> anyhow::Result<()> { - let validation_status = if tx_errors.is_empty() { - ValidationStatus::Go - } else { - error!( - "Tx validation failed: block={}, bad_transactions={}", - block.number, - tx_errors - .iter() - .map(|(tx_index, error)| format!("tx-index={tx_index}, error={error}")) - .collect::>() - .join("; "), - ); - ValidationStatus::NoGo(ValidationError::BadTransactions { - bad_transactions: tx_errors, - }) - }; - self.context - .message_bus - .publish( - &self.topic, - Arc::new(Message::Cardano(( - block.clone(), - CardanoMessage::BlockValidation(validation_status), - ))), - ) - .await - } -} diff --git a/modules/tx_unpacker/src/utils.rs b/modules/tx_unpacker/src/utils.rs new file mode 100644 index 000000000..79c6bca9b --- /dev/null +++ b/modules/tx_unpacker/src/utils.rs @@ -0,0 +1,58 @@ +use std::collections::HashSet; +use tracing::error; + +use acropolis_common::{ + protocol_params::ProtocolParams, AlonzoBabbageUpdateProposal, KeyHash, ScriptHash, + TxCertificateWithPos, Withdrawal, +}; + +/// Get VKey Witnesses needed for transaction +/// Get Scripts needed for transaction +/// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/UTxO.hs#L274 +/// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/UTxO.hs#L226 +/// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/UTxO.hs#L103 +/// +/// VKey Witnesses needed +/// 1. UTxO authors: keys that own the UTxO being spent +/// 2. Certificate authors: keys authorizing certificates +/// 3. Pool owners: owners that must sign pool registration +/// 4. Withdrawal authors: keys authorizing reward withdrawals +/// 5. Governance authors: keys authorizing governance actions (e.g. protocol update) +/// +/// Script Witnesses needed +/// 1. Input scripts: scripts locking UTxO being spent +/// 2. Withdrawal scripts: scripts controlling reward accounts +/// 3. Certificate scripts: scripts in certificate credentials. +/// +/// NOTE: +/// This doesn't count `inputs` +/// which will be considered in the utxos_state +pub fn get_vkey_script_needed( + certs: &[TxCertificateWithPos], + withdrawals: &[Withdrawal], + proposal_update: &Option, + protocol_params: &ProtocolParams, + vkey_hashes: &mut HashSet, + script_hashes: &mut HashSet, +) { + let genesis_delegs = + protocol_params.shelley.as_ref().map(|shelley_params| &shelley_params.gen_delegs); + // for each certificate, get the required vkey and script hashes + for cert_with_pos in certs.iter() { + cert_with_pos.cert.get_cert_authors(vkey_hashes, script_hashes); + } + + // for each withdrawal, get the required vkey and script hashes + for withdrawal in withdrawals.iter() { + withdrawal.get_withdrawal_authors(vkey_hashes, script_hashes); + } + + // for each governance action, get the required vkey hashes + if let Some(proposal_update) = proposal_update.as_ref() { + if let Some(genesis_delegs) = genesis_delegs { + proposal_update.get_governance_authors(vkey_hashes, genesis_delegs); + } else { + error!("Genesis delegates not found in protocol parameters"); + } + } +} diff --git a/modules/tx_unpacker/src/validations/shelley/tx.rs b/modules/tx_unpacker/src/validations/shelley/tx.rs index 64c9a13e7..84c538fce 100644 --- a/modules/tx_unpacker/src/validations/shelley/tx.rs +++ b/modules/tx_unpacker/src/validations/shelley/tx.rs @@ -85,11 +85,11 @@ mod tests { use pallas::ledger::traverse::{Era as PallasEra, MultiEraTx}; use test_case::test_case; - #[test_case(validation_fixture!("cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f") => + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e") => matches Ok(()); - "valid transaction 1" + "valid transaction 1 - with byron input & output" )] - #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e") => + #[test_case(validation_fixture!("da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b") => matches Ok(()); "valid transaction 2" )] diff --git a/modules/tx_unpacker/src/validations/shelley/utxo.rs b/modules/tx_unpacker/src/validations/shelley/utxo.rs index fe679ac86..9f06ad415 100644 --- a/modules/tx_unpacker/src/validations/shelley/utxo.rs +++ b/modules/tx_unpacker/src/validations/shelley/utxo.rs @@ -180,11 +180,11 @@ mod tests { use pallas::ledger::traverse::{Era as PallasEra, MultiEraTx}; use test_case::test_case; - #[test_case(validation_fixture!("cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f") => + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e") => matches Ok(()); - "valid transaction 1" + "valid transaction 1 - with byron input & output" )] - #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e") => + #[test_case(validation_fixture!("da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b") => matches Ok(()); "valid transaction 2" )] diff --git a/modules/tx_unpacker/src/validations/shelley/utxow.rs b/modules/tx_unpacker/src/validations/shelley/utxow.rs index e4009df75..0d691d626 100644 --- a/modules/tx_unpacker/src/validations/shelley/utxow.rs +++ b/modules/tx_unpacker/src/validations/shelley/utxow.rs @@ -135,3 +135,50 @@ pub fn validate( Ok(()) } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + use crate::{test_utils::TestContext, validation_fixture}; + use pallas::ledger::traverse::{Era as PallasEra, MultiEraTx}; + use test_case::test_case; + + #[test_case(validation_fixture!("20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e") => + matches Ok(()); + "valid transaction 1 - with byron input & output" + )] + #[test_case(validation_fixture!("da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b") => + matches Ok(()); + "valid transaction 2" + )] + #[test_case(validation_fixture!("0c993cb361c213e5b04d241321975e22870a0d658c03ea5b817c24fc48252ea0") => + matches Ok(()); + "valid transaction 3 - with mir certificates" + )] + #[test_case(validation_fixture!("0c993cb361c213e5b04d241321975e22870a0d658c03ea5b817c24fc48252ea0", "mir_insufficient_genesis_sigs_utxow") => + matches Err(UTxOWValidationError::MIRInsufficientGenesisSigsUTXOW { genesis_keys, quorum: 5 }) + if genesis_keys.len() == 4; + "mir_insufficient_genesis_sigs_utxow - 4 genesis sigs" + )] + #[test_case(validation_fixture!("da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b", "invalid_witnesses_utxow") => + matches Err(UTxOWValidationError::InvalidWitnessesUTxOW { key_hash, .. }) + if key_hash == KeyHash::from_str("b0baefb8dedefd7ec935514696ea5a66e9520f31dc8867737f0f0084").unwrap(); + "invalid_witnesses_utxow" + )] + #[allow(clippy::result_large_err)] + fn shelley_test((ctx, raw_tx): (TestContext, Vec)) -> Result<(), UTxOWValidationError> { + let tx = MultiEraTx::decode_for_era(PallasEra::Shelley, &raw_tx).unwrap(); + let mtx = tx.as_alonzo().unwrap(); + let vkey_witnesses = acropolis_codec::map_vkey_witnesses(tx.vkey_witnesses()).0; + validate( + mtx, + TxHash::from(*tx.hash()), + &vkey_witnesses, + &ctx.shelley_params.gen_delegs, + ctx.shelley_params.update_quorum, + ) + .map_err(|e| *e) + } +} diff --git a/modules/tx_unpacker/tests/data/0c993cb361c213e5b04d241321975e22870a0d658c03ea5b817c24fc48252ea0/context.json b/modules/tx_unpacker/tests/data/0c993cb361c213e5b04d241321975e22870a0d658c03ea5b817c24fc48252ea0/context.json new file mode 100644 index 000000000..5e631e9fc --- /dev/null +++ b/modules/tx_unpacker/tests/data/0c993cb361c213e5b04d241321975e22870a0d658c03ea5b817c24fc48252ea0/context.json @@ -0,0 +1,76 @@ +{ + "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": { + "ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c": { + "delegate": "d9e5c76ad5ee778960804094a389f0b546b5c2b140a62f8ec43ea54d", + "vrf": "64fa87e8b29a5b7bfbd6795677e3e878c505bc4a3649485d366b50abadec92d7" + }, + "b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497": { + "delegate": "855d6fc1e54274e331e34478eeac8d060b0b90c1f9e8a2b01167c048", + "vrf": "66d5167a1f426bd1adcc8bbf4b88c280d38c148d135cb41e3f5a39f948ad7fcc" + }, + "60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1": { + "delegate": "7f72a1826ae3b279782ab2bc582d0d2958de65bd86b2c4f82d8ba956", + "vrf": "c0546d9aa5740afd569d3c2d9c412595cd60822bb6d9a4e8ce6c43d12bd0f674" + }, + "f7b341c14cd58fca4195a9b278cce1ef402dc0e06deb77e543cd1757": { + "delegate": "69ae12f9e45c0c9122356c8e624b1fbbed6c22a2e3b4358cf0cb5011", + "vrf": "6394a632af51a32768a6f12dac3485d9c0712d0b54e3f389f355385762a478f2" + }, + "162f94554ac8c225383a2248c245659eda870eaa82d0ef25fc7dcd82": { + "delegate": "4485708022839a7b9b8b639a939c85ec0ed6999b5b6dc651b03c43f6", + "vrf": "aba81e764b71006c515986bf7b37a72fbb5554f78e6775f08e384dbd572a4b32" + }, + "2075a095b3c844a29c24317a94a643ab8e22d54a3a3a72a420260af6": { + "delegate": "6535db26347283990a252313a7903a45e3526ec25ddba381c071b25b", + "vrf": "fcaca997b8105bd860876348fc2c6e68b13607f9bbd23515cd2193b555d267af" + }, + "268cfc0b89e910ead22e0ade91493d8212f53f3e2164b2e4bef0819b": { + "delegate": "1d4f2e1fda43070d71bb22a5522f86943c7c18aeb4fa47a362c27e23", + "vrf": "63ef48bc5355f3e7973100c371d6a095251c80ceb40559f4750aa7014a6fb6db" + } + } + }, + "utxos": [ + [ + ["278ec9a3dfb551288affd8d84b2d6c70b56a153ad1fd43e7b18a5528f687733e", 1], + [4616843, 0] + ] + ] +} diff --git a/modules/tx_unpacker/tests/data/0c993cb361c213e5b04d241321975e22870a0d658c03ea5b817c24fc48252ea0/mir_insufficient_genesis_sigs_utxow.cbor b/modules/tx_unpacker/tests/data/0c993cb361c213e5b04d241321975e22870a0d658c03ea5b817c24fc48252ea0/mir_insufficient_genesis_sigs_utxow.cbor new file mode 100644 index 000000000..26068d317 --- /dev/null +++ b/modules/tx_unpacker/tests/data/0c993cb361c213e5b04d241321975e22870a0d658c03ea5b817c24fc48252ea0/mir_insufficient_genesis_sigs_utxow.cbor @@ -0,0 +1 @@ +84a50081825820a18b44236a1e9e313a366bbf4f28f845ed18c635f384182953b8f719b93e607f00018182581d61d80fe69ded1ff90f41e526d0332a2ff98ba8a0d85ceb8941b51784201b00000005f440f51b021a000dbba0031a00989680049f82068200bf8200581c0a540f850729cab957a6ca7eec760eb1db048d67248018b1918e5eb51a2d9992228200581c0e745bad8dc72b95a2b491866cd727b7b582a7b3985a438d20401be71a002f96db8200581c112184e2a7c0223378aeb8ce8d4e830fe8a98cdad09393b28284f6421a012b0fb98200581c13590612ff22acc5aa253f428eee2f397041c186a2972f5c9a7e39161a0080bbeb8200581c16026d74d8609084862b165a23e83be97c536a7eb19ba0ca6244685c1a0c7ffdf88200581c1c494a8d2e382642819c49c7573bec4f362b27f488f97de6e211e7f11a001ecda88200581c1ecd0fac53f81e5f3a38ffb35a09f04b94ae0083f934522cb66bfcce1a0002f2268200581c2c201b342429d30945f2b35947b97a181dca4814a1c7fdf13663e4421a00097c058200581c2c5af834cd4f40636a0d78c1fb99b929d4aee22b811407ea3f26a8c01a00032cfd8200581c30045906d5114264d65f4f7439e02a03b71087b28aed9c5d644fb0b81a010b36968200581c308ccce6c22e2e7707e452d5d1f74e376a271790a391b27f3b36545f1a001582c38200581c36794c9b6a9aa6c951df4f8ad74fa722d6700fd52b5bd4b46215949b1a05e28c9c8200581c3a0f567ab6efb7aee1b3bb7e3cc71650dd452d6793ea6729e37b43711a00178d7a8200581c3af912a30072d4e14d1a4fffd6b3b819cf69d75920a1a9a6dfe3ccc21a03ca0c8d8200581c3ba071bffc8168470cf32300caa10826f3cbe4a9b336d97661aa840e1a0200e94a8200581c3d518c7074392e75f67b2dc1b780a574f296749f248658dc944001471a003ae9b88200581c439fde47ee96165528cc9e49ed0a815799091b9a3d1c596b29da465c1a0004fcba8200581c43fac797757d9d8f05225f1c8e9daffa41be947b7b24343c75aa95b91a009f6e818200581c46c4569d7cdd196745e759e9eb4427dad782453bb72370202c2d3bf71a0c4a3c938200581c4a9c8b9fb60e61494195bdc3964ea4b4545b4e6b4bf7bd91297cfa1c1a007fd8008200581c50914d948b2a2396d816fa286c63a80ad34d4313faabcc6ae6572ab31a0002a8848200581c509de94fb26848415ae89a58b4550fb3260e975d8f7923edabe2a1dd1a0003209d8200581c53dd2df4d6546eecc53015a577df47128fbd777a74988e6b3ec60cef1a0018ae828200581c5bf4e78da823c406f9e0a868d7857cff81c0a93c48347d4caefdc9e81a02cf28d18200581c5ed0c60752f4246e6cc48858f927ee433a2f37fefd2aba848cbd22a71a0112b2f68200581c613b03c114cbeb0ad62748eef835836281d4be80c0902e183b0c7ad81a008faf698200581c64602a9e19420ed4073ef18427403ab908f56752b12666781f5ddaf21a002150e98200581c663440a4ec794bb75f2e56c56a77eeaec600576ce789d91e608df7861a0002d05c8200581c663cc4d6df2a3ea21b4204ed4ee67038287c854abd8d423d1b434c101a2d80d6de8200581c6c6fd40313ff7632fce3c594dcbe36b40d963b83fe2f489b3a7840cc1a00374c388200581c6d29b3990b9a925bf90162d66fa34928682c906e1483ebb239af2c621a00ac4e238200581c6e1991156c9b8cded86f0e084d25f3a4a8dc2380eabb4a780f9f99aa1937b08200581c6ec2dfaa0ede63f7105a0edf27bb901f1e24c70a46a702542f1648c41a00ba69cb8200581c72ef8ad541a168f1c68cdf4a8e84e069ae231e92d6454d3cdccf51851a004fae218200581c7387f85e9c5d3b7568514efee42ee3d8b69b8e18aca721a45a8b85241a003c19828200581c7426e2b942d46f315e1d8856c57c66119d91ea7de38a5a149f9154651a001e391e8200581c7f810ff2066672e71cc2e5364edf27815031cc00abd30f8d85cf56051a03499fcb8200581c862c9c742fba89a4cbacc0a8ce7a48fa91533933bfb8b55f448ac1cd1a007cb79e8200581c89cf064ed3b3b0978088e76044bf17e336aae6e2dd760e4a70016c171a04155a058200581c9f6ac0212d3b4072f96515a58aeaf75f0a1eeeacb78497890ef8c0411a00046cd18200581ca1e7f2db8ee6a552bb38355d550e1b99417e8fc469b8c09e460603ce1a0dbda7ec8200581ca2f3945a526cf607e3b0517e88b63152ea71d76225231ec240ab17981a03ec7f3f8200581ca327881124808ba607cf0c1d572919bad5dfa06487403035e4b8a9161a000924a58200581ca7433c97681d4e4560ca81d8a8753259b22546bb2703040644850e871a0001a0308200581ca7710e716598ec890417744ced69d16b245e4d8713750558a29baa081a000330138200581cab0cb981bfd164c061400101f42ef302e8dbcd537944531f64c1bb181a0014ffbc8200581caf15aa3d6c0d6621a8c60f8a299f198fdaf9ba340258839f91677f611a00d76a468200581cb423ba18627af608afbaff3dcd251a9241a92514e80e61a12f53e7651a0001ce1a8200581cba0a3213303b8830ed912582971ff32859a65ac6b88c7101e38ca1b11a0007346b8200581cbb51baf971e8b361042c5b8fe770afe4099e3aab4966335e72a5b4461a015623d38200581cbcefbe1b2b042efa3f7e3756b4f229e60f0889b6a8286d76354809631a00111a398200581cbd4d18050d207d85062843b05098d8798e337aef4ce060e3d3551e9a1a00029ee88200581cbe05c2bba7bb18d17b47a92556618a0dd6c7bdc12c213d484939e1db1a00572afa8200581cbe49cf8aab10a2261054f2cc82a989eb5a90f13c696185aa9073e1351a00026a418200581cbe93b7c20c5a863ce9fcc276faa9824cca5dc525826c870b35ba3e741a0073a1fd8200581cc1350c365dc2f89df871bbd716c04a021d4a056edfd2ac7786e256741a0006d5578200581cc17684bb6cbd8a8c79a594c361b33a70b566180385311a2a0a3144cf1a00f391198200581cca0971ef40ae56c31c5bbeba8cfd6ec0b131790c10f80e64727c47d11a00ae2ea08200581cd050165a8e2c9ffd79219c00fce8cb4c811512ca97c1354556b0db161a026593c68200581cd39b6de0693322e92d3309aafabc33a9dd214ea5eddb2fe4ea62715d1a0003cd8b8200581cd3df6c7089b126b2658e304282908e604541af6f619a84ca8dd79ba41976858200581cd451578a2922af75e02a4204227925dfc0db3698d480409398ba95ce1a0424d4458200581cd4aa7428d4b7e0b5203a5509d948b13064469473205ca423b245fb121a000ef32b8200581cd6a00b9fef1dfdd16fbed017368454d71c96e2510104912157b5492e19f6ab8200581cd926d761bb6be20094c2125e17fa83d5079cb404bae0ac339c5fbeb71a012964248200581cdaed75cc22badbf64a766d530431f98408ea6208097c57e5edbd50bd1a008536ae8200581ce7288695dc7d3135c05bf1964cc2f9ee7e6c4488ce9576a1e70e4cfc1a0050052b8200581ce8686f66d5e31d8d8dc83928989bdf0ffec735e98fa4c463f0f4e1631a00074f598200581ce9a4409b1a8e291b1c8f69bdfc325b3cd5356d3a1674c02c9054b65f1a00eadb8f8200581ce9b17d077311561774a88b439edf4d5599d72ecd4573e15aac8ba48319159c8200581cefab106eb4ca809202d5f3d2b4658c37aa6f9d8fc2e56c7543d765f91a0027ca228200581cefb0b41b3b938fe265c828a516d105660861f5c8d19f2b3a6a89bd161a003283fd8200581cfbf9ddf9e08ed86b74dc070c43e8f4871f7f21f5ac45ae182672e4741a003e66b58200581cff613aeb57ae6850eec49e04ff665d95218aceb1f0ee7393f44b1f311a00774ac4ff82008200581c0e745bad8dc72b95a2b491866cd727b7b582a7b3985a438d20401be782008200581c43fac797757d9d8f05225f1c8e9daffa41be947b7b24343c75aa95b982008200581c13590612ff22acc5aa253f428eee2f397041c186a2972f5c9a7e391682008200581c3af912a30072d4e14d1a4fffd6b3b819cf69d75920a1a9a6dfe3ccc282008200581cd926d761bb6be20094c2125e17fa83d5079cb404bae0ac339c5fbeb782008200581c6e1991156c9b8cded86f0e084d25f3a4a8dc2380eabb4a780f9f99aa82008200581cb423ba18627af608afbaff3dcd251a9241a92514e80e61a12f53e76582008200581cefb0b41b3b938fe265c828a516d105660861f5c8d19f2b3a6a89bd1682008200581cab0cb981bfd164c061400101f42ef302e8dbcd537944531f64c1bb1882008200581c53dd2df4d6546eecc53015a577df47128fbd777a74988e6b3ec60cef82008200581c46c4569d7cdd196745e759e9eb4427dad782453bb72370202c2d3bf782008200581c663cc4d6df2a3ea21b4204ed4ee67038287c854abd8d423d1b434c1082008200581cbe49cf8aab10a2261054f2cc82a989eb5a90f13c696185aa9073e13582008200581cdaed75cc22badbf64a766d530431f98408ea6208097c57e5edbd50bd82008200581c36794c9b6a9aa6c951df4f8ad74fa722d6700fd52b5bd4b46215949b82008200581cd451578a2922af75e02a4204227925dfc0db3698d480409398ba95ce82008200581c308ccce6c22e2e7707e452d5d1f74e376a271790a391b27f3b36545f82008200581c16026d74d8609084862b165a23e83be97c536a7eb19ba0ca6244685c82008200581cff613aeb57ae6850eec49e04ff665d95218aceb1f0ee7393f44b1f3182008200581c7387f85e9c5d3b7568514efee42ee3d8b69b8e18aca721a45a8b852482008200581cc17684bb6cbd8a8c79a594c361b33a70b566180385311a2a0a3144cf82008200581c112184e2a7c0223378aeb8ce8d4e830fe8a98cdad09393b28284f64282008200581cbb51baf971e8b361042c5b8fe770afe4099e3aab4966335e72a5b44682008200581ca1e7f2db8ee6a552bb38355d550e1b99417e8fc469b8c09e460603ce82008200581c1c494a8d2e382642819c49c7573bec4f362b27f488f97de6e211e7f182008200581c5bf4e78da823c406f9e0a868d7857cff81c0a93c48347d4caefdc9e882008200581c6d29b3990b9a925bf90162d66fa34928682c906e1483ebb239af2c6282008200581cefab106eb4ca809202d5f3d2b4658c37aa6f9d8fc2e56c7543d765f982008200581cd050165a8e2c9ffd79219c00fce8cb4c811512ca97c1354556b0db1682008200581c3ba071bffc8168470cf32300caa10826f3cbe4a9b336d97661aa840e82008200581c862c9c742fba89a4cbacc0a8ce7a48fa91533933bfb8b55f448ac1cd82008200581c30045906d5114264d65f4f7439e02a03b71087b28aed9c5d644fb0b882008200581c0a540f850729cab957a6ca7eec760eb1db048d67248018b1918e5eb582008200581c3d518c7074392e75f67b2dc1b780a574f296749f248658dc9440014782008200581c7f810ff2066672e71cc2e5364edf27815031cc00abd30f8d85cf560582008200581ce7288695dc7d3135c05bf1964cc2f9ee7e6c4488ce9576a1e70e4cfc82008200581c72ef8ad541a168f1c68cdf4a8e84e069ae231e92d6454d3cdccf518582008200581c6ec2dfaa0ede63f7105a0edf27bb901f1e24c70a46a702542f1648c482008200581c6c6fd40313ff7632fce3c594dcbe36b40d963b83fe2f489b3a7840cc82008200581ce9b17d077311561774a88b439edf4d5599d72ecd4573e15aac8ba48382008200581ce8686f66d5e31d8d8dc83928989bdf0ffec735e98fa4c463f0f4e16382008200581cca0971ef40ae56c31c5bbeba8cfd6ec0b131790c10f80e64727c47d182008200581c9f6ac0212d3b4072f96515a58aeaf75f0a1eeeacb78497890ef8c04182008200581ce9a4409b1a8e291b1c8f69bdfc325b3cd5356d3a1674c02c9054b65f82008200581c4a9c8b9fb60e61494195bdc3964ea4b4545b4e6b4bf7bd91297cfa1c82008200581c50914d948b2a2396d816fa286c63a80ad34d4313faabcc6ae6572ab382008200581cd4aa7428d4b7e0b5203a5509d948b13064469473205ca423b245fb1282008200581c509de94fb26848415ae89a58b4550fb3260e975d8f7923edabe2a1dd82008200581c439fde47ee96165528cc9e49ed0a815799091b9a3d1c596b29da465c82008200581cbe05c2bba7bb18d17b47a92556618a0dd6c7bdc12c213d484939e1db82008200581ca7433c97681d4e4560ca81d8a8753259b22546bb2703040644850e8782008200581cd3df6c7089b126b2658e304282908e604541af6f619a84ca8dd79ba482008200581cd6a00b9fef1dfdd16fbed017368454d71c96e2510104912157b5492e82008200581caf15aa3d6c0d6621a8c60f8a299f198fdaf9ba340258839f91677f6182008200581c2c5af834cd4f40636a0d78c1fb99b929d4aee22b811407ea3f26a8c082008200581c1ecd0fac53f81e5f3a38ffb35a09f04b94ae0083f934522cb66bfcce82008200581c89cf064ed3b3b0978088e76044bf17e336aae6e2dd760e4a70016c1782008200581c613b03c114cbeb0ad62748eef835836281d4be80c0902e183b0c7ad882008200581ca2f3945a526cf607e3b0517e88b63152ea71d76225231ec240ab179882008200581cd39b6de0693322e92d3309aafabc33a9dd214ea5eddb2fe4ea62715d82008200581c2c201b342429d30945f2b35947b97a181dca4814a1c7fdf13663e44282008200581cbcefbe1b2b042efa3f7e3756b4f229e60f0889b6a8286d763548096382008200581cbe93b7c20c5a863ce9fcc276faa9824cca5dc525826c870b35ba3e7482008200581cba0a3213303b8830ed912582971ff32859a65ac6b88c7101e38ca1b182008200581c3a0f567ab6efb7aee1b3bb7e3cc71650dd452d6793ea6729e37b437182008200581cfbf9ddf9e08ed86b74dc070c43e8f4871f7f21f5ac45ae182672e47482008200581ca7710e716598ec890417744ced69d16b245e4d8713750558a29baa0882008200581c64602a9e19420ed4073ef18427403ab908f56752b12666781f5ddaf282008200581ca327881124808ba607cf0c1d572919bad5dfa06487403035e4b8a91682008200581c5ed0c60752f4246e6cc48858f927ee433a2f37fefd2aba848cbd22a782008200581c663440a4ec794bb75f2e56c56a77eeaec600576ce789d91e608df78682008200581c7426e2b942d46f315e1d8856c57c66119d91ea7de38a5a149f91546582008200581cbd4d18050d207d85062843b05098d8798e337aef4ce060e3d3551e9a82008200581cc1350c365dc2f89df871bbd716c04a021d4a056edfd2ac7786e25674ffa1008582582061261a95b7613ee6bf2067dad77b70349729b0c50d57bc1cf30de0db4a1e73a85840547ca0683dca695d4883042c014688f238465d79fad43bb97856865a80a79ece2951b264e3cd0e8848b45196be5d6b92dc1c1d87e69e27d6aa1188fb756795088258209180d818e69cd997e34663c418a648c076f2e19cd4194e486e159d8580bc6cda5840c4b56923278099b38a06f0d8ec176f3084c123ee5146d22ca1689cc4aec2fb0a02eb6015914fb4ade6628960ea0012f472a454c03a6f61d42859f0118591e10082582089c29f8c4af27b7accbe589747820134ebbaa1caf3ce949270a3d0c7dcfd541b58409c87502c394dcb0c8e990d460e0c4f17c62c5ba6123a24c395d0227a971c30d4dc99c6e9eef42ee6cceb5394cd399641f4fe3b415372d5bca45b982a53cdaa0f8258208b53207629f9a30e4b2015044f337c01735abe67243c19470c9dae8c7b7327985840f1c8c83860628d2e8f66c7aed9bb99aa9cc24db5058fa8fe9757da3b7165799276f5c5d582df73e2dd0cb4b3da21976a824f4a7c3c4b499908d06a70f6a5da0b825820cbc6b506e94fbefe442eecee376f3b3ebaf89415ef5cd2efb666e06ddae4839358404a628159b631bc62bd840282ba78c629b18f8e066c0d39da37e15eab8639e277da1d0f67c9e173e7e7c2d9f17808efe3e4f6bb4872ecb2373ecc8ee1110e4d02f5f6 \ No newline at end of file diff --git a/modules/tx_unpacker/tests/data/0c993cb361c213e5b04d241321975e22870a0d658c03ea5b817c24fc48252ea0/tx.cbor b/modules/tx_unpacker/tests/data/0c993cb361c213e5b04d241321975e22870a0d658c03ea5b817c24fc48252ea0/tx.cbor new file mode 100644 index 000000000..ed55495ff --- /dev/null +++ b/modules/tx_unpacker/tests/data/0c993cb361c213e5b04d241321975e22870a0d658c03ea5b817c24fc48252ea0/tx.cbor @@ -0,0 +1 @@ +84a50081825820a18b44236a1e9e313a366bbf4f28f845ed18c635f384182953b8f719b93e607f00018182581d61d80fe69ded1ff90f41e526d0332a2ff98ba8a0d85ceb8941b51784201b00000005f440f51b021a000dbba0031a00989680049f82068200bf8200581c0a540f850729cab957a6ca7eec760eb1db048d67248018b1918e5eb51a2d9992228200581c0e745bad8dc72b95a2b491866cd727b7b582a7b3985a438d20401be71a002f96db8200581c112184e2a7c0223378aeb8ce8d4e830fe8a98cdad09393b28284f6421a012b0fb98200581c13590612ff22acc5aa253f428eee2f397041c186a2972f5c9a7e39161a0080bbeb8200581c16026d74d8609084862b165a23e83be97c536a7eb19ba0ca6244685c1a0c7ffdf88200581c1c494a8d2e382642819c49c7573bec4f362b27f488f97de6e211e7f11a001ecda88200581c1ecd0fac53f81e5f3a38ffb35a09f04b94ae0083f934522cb66bfcce1a0002f2268200581c2c201b342429d30945f2b35947b97a181dca4814a1c7fdf13663e4421a00097c058200581c2c5af834cd4f40636a0d78c1fb99b929d4aee22b811407ea3f26a8c01a00032cfd8200581c30045906d5114264d65f4f7439e02a03b71087b28aed9c5d644fb0b81a010b36968200581c308ccce6c22e2e7707e452d5d1f74e376a271790a391b27f3b36545f1a001582c38200581c36794c9b6a9aa6c951df4f8ad74fa722d6700fd52b5bd4b46215949b1a05e28c9c8200581c3a0f567ab6efb7aee1b3bb7e3cc71650dd452d6793ea6729e37b43711a00178d7a8200581c3af912a30072d4e14d1a4fffd6b3b819cf69d75920a1a9a6dfe3ccc21a03ca0c8d8200581c3ba071bffc8168470cf32300caa10826f3cbe4a9b336d97661aa840e1a0200e94a8200581c3d518c7074392e75f67b2dc1b780a574f296749f248658dc944001471a003ae9b88200581c439fde47ee96165528cc9e49ed0a815799091b9a3d1c596b29da465c1a0004fcba8200581c43fac797757d9d8f05225f1c8e9daffa41be947b7b24343c75aa95b91a009f6e818200581c46c4569d7cdd196745e759e9eb4427dad782453bb72370202c2d3bf71a0c4a3c938200581c4a9c8b9fb60e61494195bdc3964ea4b4545b4e6b4bf7bd91297cfa1c1a007fd8008200581c50914d948b2a2396d816fa286c63a80ad34d4313faabcc6ae6572ab31a0002a8848200581c509de94fb26848415ae89a58b4550fb3260e975d8f7923edabe2a1dd1a0003209d8200581c53dd2df4d6546eecc53015a577df47128fbd777a74988e6b3ec60cef1a0018ae828200581c5bf4e78da823c406f9e0a868d7857cff81c0a93c48347d4caefdc9e81a02cf28d18200581c5ed0c60752f4246e6cc48858f927ee433a2f37fefd2aba848cbd22a71a0112b2f68200581c613b03c114cbeb0ad62748eef835836281d4be80c0902e183b0c7ad81a008faf698200581c64602a9e19420ed4073ef18427403ab908f56752b12666781f5ddaf21a002150e98200581c663440a4ec794bb75f2e56c56a77eeaec600576ce789d91e608df7861a0002d05c8200581c663cc4d6df2a3ea21b4204ed4ee67038287c854abd8d423d1b434c101a2d80d6de8200581c6c6fd40313ff7632fce3c594dcbe36b40d963b83fe2f489b3a7840cc1a00374c388200581c6d29b3990b9a925bf90162d66fa34928682c906e1483ebb239af2c621a00ac4e238200581c6e1991156c9b8cded86f0e084d25f3a4a8dc2380eabb4a780f9f99aa1937b08200581c6ec2dfaa0ede63f7105a0edf27bb901f1e24c70a46a702542f1648c41a00ba69cb8200581c72ef8ad541a168f1c68cdf4a8e84e069ae231e92d6454d3cdccf51851a004fae218200581c7387f85e9c5d3b7568514efee42ee3d8b69b8e18aca721a45a8b85241a003c19828200581c7426e2b942d46f315e1d8856c57c66119d91ea7de38a5a149f9154651a001e391e8200581c7f810ff2066672e71cc2e5364edf27815031cc00abd30f8d85cf56051a03499fcb8200581c862c9c742fba89a4cbacc0a8ce7a48fa91533933bfb8b55f448ac1cd1a007cb79e8200581c89cf064ed3b3b0978088e76044bf17e336aae6e2dd760e4a70016c171a04155a058200581c9f6ac0212d3b4072f96515a58aeaf75f0a1eeeacb78497890ef8c0411a00046cd18200581ca1e7f2db8ee6a552bb38355d550e1b99417e8fc469b8c09e460603ce1a0dbda7ec8200581ca2f3945a526cf607e3b0517e88b63152ea71d76225231ec240ab17981a03ec7f3f8200581ca327881124808ba607cf0c1d572919bad5dfa06487403035e4b8a9161a000924a58200581ca7433c97681d4e4560ca81d8a8753259b22546bb2703040644850e871a0001a0308200581ca7710e716598ec890417744ced69d16b245e4d8713750558a29baa081a000330138200581cab0cb981bfd164c061400101f42ef302e8dbcd537944531f64c1bb181a0014ffbc8200581caf15aa3d6c0d6621a8c60f8a299f198fdaf9ba340258839f91677f611a00d76a468200581cb423ba18627af608afbaff3dcd251a9241a92514e80e61a12f53e7651a0001ce1a8200581cba0a3213303b8830ed912582971ff32859a65ac6b88c7101e38ca1b11a0007346b8200581cbb51baf971e8b361042c5b8fe770afe4099e3aab4966335e72a5b4461a015623d38200581cbcefbe1b2b042efa3f7e3756b4f229e60f0889b6a8286d76354809631a00111a398200581cbd4d18050d207d85062843b05098d8798e337aef4ce060e3d3551e9a1a00029ee88200581cbe05c2bba7bb18d17b47a92556618a0dd6c7bdc12c213d484939e1db1a00572afa8200581cbe49cf8aab10a2261054f2cc82a989eb5a90f13c696185aa9073e1351a00026a418200581cbe93b7c20c5a863ce9fcc276faa9824cca5dc525826c870b35ba3e741a0073a1fd8200581cc1350c365dc2f89df871bbd716c04a021d4a056edfd2ac7786e256741a0006d5578200581cc17684bb6cbd8a8c79a594c361b33a70b566180385311a2a0a3144cf1a00f391198200581cca0971ef40ae56c31c5bbeba8cfd6ec0b131790c10f80e64727c47d11a00ae2ea08200581cd050165a8e2c9ffd79219c00fce8cb4c811512ca97c1354556b0db161a026593c68200581cd39b6de0693322e92d3309aafabc33a9dd214ea5eddb2fe4ea62715d1a0003cd8b8200581cd3df6c7089b126b2658e304282908e604541af6f619a84ca8dd79ba41976858200581cd451578a2922af75e02a4204227925dfc0db3698d480409398ba95ce1a0424d4458200581cd4aa7428d4b7e0b5203a5509d948b13064469473205ca423b245fb121a000ef32b8200581cd6a00b9fef1dfdd16fbed017368454d71c96e2510104912157b5492e19f6ab8200581cd926d761bb6be20094c2125e17fa83d5079cb404bae0ac339c5fbeb71a012964248200581cdaed75cc22badbf64a766d530431f98408ea6208097c57e5edbd50bd1a008536ae8200581ce7288695dc7d3135c05bf1964cc2f9ee7e6c4488ce9576a1e70e4cfc1a0050052b8200581ce8686f66d5e31d8d8dc83928989bdf0ffec735e98fa4c463f0f4e1631a00074f598200581ce9a4409b1a8e291b1c8f69bdfc325b3cd5356d3a1674c02c9054b65f1a00eadb8f8200581ce9b17d077311561774a88b439edf4d5599d72ecd4573e15aac8ba48319159c8200581cefab106eb4ca809202d5f3d2b4658c37aa6f9d8fc2e56c7543d765f91a0027ca228200581cefb0b41b3b938fe265c828a516d105660861f5c8d19f2b3a6a89bd161a003283fd8200581cfbf9ddf9e08ed86b74dc070c43e8f4871f7f21f5ac45ae182672e4741a003e66b58200581cff613aeb57ae6850eec49e04ff665d95218aceb1f0ee7393f44b1f311a00774ac4ff82008200581c0e745bad8dc72b95a2b491866cd727b7b582a7b3985a438d20401be782008200581c43fac797757d9d8f05225f1c8e9daffa41be947b7b24343c75aa95b982008200581c13590612ff22acc5aa253f428eee2f397041c186a2972f5c9a7e391682008200581c3af912a30072d4e14d1a4fffd6b3b819cf69d75920a1a9a6dfe3ccc282008200581cd926d761bb6be20094c2125e17fa83d5079cb404bae0ac339c5fbeb782008200581c6e1991156c9b8cded86f0e084d25f3a4a8dc2380eabb4a780f9f99aa82008200581cb423ba18627af608afbaff3dcd251a9241a92514e80e61a12f53e76582008200581cefb0b41b3b938fe265c828a516d105660861f5c8d19f2b3a6a89bd1682008200581cab0cb981bfd164c061400101f42ef302e8dbcd537944531f64c1bb1882008200581c53dd2df4d6546eecc53015a577df47128fbd777a74988e6b3ec60cef82008200581c46c4569d7cdd196745e759e9eb4427dad782453bb72370202c2d3bf782008200581c663cc4d6df2a3ea21b4204ed4ee67038287c854abd8d423d1b434c1082008200581cbe49cf8aab10a2261054f2cc82a989eb5a90f13c696185aa9073e13582008200581cdaed75cc22badbf64a766d530431f98408ea6208097c57e5edbd50bd82008200581c36794c9b6a9aa6c951df4f8ad74fa722d6700fd52b5bd4b46215949b82008200581cd451578a2922af75e02a4204227925dfc0db3698d480409398ba95ce82008200581c308ccce6c22e2e7707e452d5d1f74e376a271790a391b27f3b36545f82008200581c16026d74d8609084862b165a23e83be97c536a7eb19ba0ca6244685c82008200581cff613aeb57ae6850eec49e04ff665d95218aceb1f0ee7393f44b1f3182008200581c7387f85e9c5d3b7568514efee42ee3d8b69b8e18aca721a45a8b852482008200581cc17684bb6cbd8a8c79a594c361b33a70b566180385311a2a0a3144cf82008200581c112184e2a7c0223378aeb8ce8d4e830fe8a98cdad09393b28284f64282008200581cbb51baf971e8b361042c5b8fe770afe4099e3aab4966335e72a5b44682008200581ca1e7f2db8ee6a552bb38355d550e1b99417e8fc469b8c09e460603ce82008200581c1c494a8d2e382642819c49c7573bec4f362b27f488f97de6e211e7f182008200581c5bf4e78da823c406f9e0a868d7857cff81c0a93c48347d4caefdc9e882008200581c6d29b3990b9a925bf90162d66fa34928682c906e1483ebb239af2c6282008200581cefab106eb4ca809202d5f3d2b4658c37aa6f9d8fc2e56c7543d765f982008200581cd050165a8e2c9ffd79219c00fce8cb4c811512ca97c1354556b0db1682008200581c3ba071bffc8168470cf32300caa10826f3cbe4a9b336d97661aa840e82008200581c862c9c742fba89a4cbacc0a8ce7a48fa91533933bfb8b55f448ac1cd82008200581c30045906d5114264d65f4f7439e02a03b71087b28aed9c5d644fb0b882008200581c0a540f850729cab957a6ca7eec760eb1db048d67248018b1918e5eb582008200581c3d518c7074392e75f67b2dc1b780a574f296749f248658dc9440014782008200581c7f810ff2066672e71cc2e5364edf27815031cc00abd30f8d85cf560582008200581ce7288695dc7d3135c05bf1964cc2f9ee7e6c4488ce9576a1e70e4cfc82008200581c72ef8ad541a168f1c68cdf4a8e84e069ae231e92d6454d3cdccf518582008200581c6ec2dfaa0ede63f7105a0edf27bb901f1e24c70a46a702542f1648c482008200581c6c6fd40313ff7632fce3c594dcbe36b40d963b83fe2f489b3a7840cc82008200581ce9b17d077311561774a88b439edf4d5599d72ecd4573e15aac8ba48382008200581ce8686f66d5e31d8d8dc83928989bdf0ffec735e98fa4c463f0f4e16382008200581cca0971ef40ae56c31c5bbeba8cfd6ec0b131790c10f80e64727c47d182008200581c9f6ac0212d3b4072f96515a58aeaf75f0a1eeeacb78497890ef8c04182008200581ce9a4409b1a8e291b1c8f69bdfc325b3cd5356d3a1674c02c9054b65f82008200581c4a9c8b9fb60e61494195bdc3964ea4b4545b4e6b4bf7bd91297cfa1c82008200581c50914d948b2a2396d816fa286c63a80ad34d4313faabcc6ae6572ab382008200581cd4aa7428d4b7e0b5203a5509d948b13064469473205ca423b245fb1282008200581c509de94fb26848415ae89a58b4550fb3260e975d8f7923edabe2a1dd82008200581c439fde47ee96165528cc9e49ed0a815799091b9a3d1c596b29da465c82008200581cbe05c2bba7bb18d17b47a92556618a0dd6c7bdc12c213d484939e1db82008200581ca7433c97681d4e4560ca81d8a8753259b22546bb2703040644850e8782008200581cd3df6c7089b126b2658e304282908e604541af6f619a84ca8dd79ba482008200581cd6a00b9fef1dfdd16fbed017368454d71c96e2510104912157b5492e82008200581caf15aa3d6c0d6621a8c60f8a299f198fdaf9ba340258839f91677f6182008200581c2c5af834cd4f40636a0d78c1fb99b929d4aee22b811407ea3f26a8c082008200581c1ecd0fac53f81e5f3a38ffb35a09f04b94ae0083f934522cb66bfcce82008200581c89cf064ed3b3b0978088e76044bf17e336aae6e2dd760e4a70016c1782008200581c613b03c114cbeb0ad62748eef835836281d4be80c0902e183b0c7ad882008200581ca2f3945a526cf607e3b0517e88b63152ea71d76225231ec240ab179882008200581cd39b6de0693322e92d3309aafabc33a9dd214ea5eddb2fe4ea62715d82008200581c2c201b342429d30945f2b35947b97a181dca4814a1c7fdf13663e44282008200581cbcefbe1b2b042efa3f7e3756b4f229e60f0889b6a8286d763548096382008200581cbe93b7c20c5a863ce9fcc276faa9824cca5dc525826c870b35ba3e7482008200581cba0a3213303b8830ed912582971ff32859a65ac6b88c7101e38ca1b182008200581c3a0f567ab6efb7aee1b3bb7e3cc71650dd452d6793ea6729e37b437182008200581cfbf9ddf9e08ed86b74dc070c43e8f4871f7f21f5ac45ae182672e47482008200581ca7710e716598ec890417744ced69d16b245e4d8713750558a29baa0882008200581c64602a9e19420ed4073ef18427403ab908f56752b12666781f5ddaf282008200581ca327881124808ba607cf0c1d572919bad5dfa06487403035e4b8a91682008200581c5ed0c60752f4246e6cc48858f927ee433a2f37fefd2aba848cbd22a782008200581c663440a4ec794bb75f2e56c56a77eeaec600576ce789d91e608df78682008200581c7426e2b942d46f315e1d8856c57c66119d91ea7de38a5a149f91546582008200581cbd4d18050d207d85062843b05098d8798e337aef4ce060e3d3551e9a82008200581cc1350c365dc2f89df871bbd716c04a021d4a056edfd2ac7786e25674ffa1008882582061261a95b7613ee6bf2067dad77b70349729b0c50d57bc1cf30de0db4a1e73a85840547ca0683dca695d4883042c014688f238465d79fad43bb97856865a80a79ece2951b264e3cd0e8848b45196be5d6b92dc1c1d87e69e27d6aa1188fb756795088258209180d818e69cd997e34663c418a648c076f2e19cd4194e486e159d8580bc6cda5840c4b56923278099b38a06f0d8ec176f3084c123ee5146d22ca1689cc4aec2fb0a02eb6015914fb4ade6628960ea0012f472a454c03a6f61d42859f0118591e10082582089c29f8c4af27b7accbe589747820134ebbaa1caf3ce949270a3d0c7dcfd541b58409c87502c394dcb0c8e990d460e0c4f17c62c5ba6123a24c395d0227a971c30d4dc99c6e9eef42ee6cceb5394cd399641f4fe3b415372d5bca45b982a53cdaa0f825820f14f712dc600d793052d4842d50cefa4e65884ea6cf83707079eb8ce302efc855840808a60c7c57eddd8b8363a94e692dd90ac3778a792c36526f20d3dd979e88966846c41a12b76642fe93117ec045adaa072bd4cd5ea62fc8a8c0ff8affb2a14048258208b53207629f9a30e4b2015044f337c01735abe67243c19470c9dae8c7b7327985840f1c8c83860628d2e8f66c7aed9bb99aa9cc24db5058fa8fe9757da3b7165799276f5c5d582df73e2dd0cb4b3da21976a824f4a7c3c4b499908d06a70f6a5da0b8258205fddeedade2714d6db2f9e1104743d2d8d818ecddc306e176108db14caadd44158402e93cc507ed77e61f6e7796699231fa55ded85fdc95b09e45ff5c6ed3833028470d234a85a99c82eeb06405058d408d9a74a50498a6222a3935c895bfd70a20d825820cbc6b506e94fbefe442eecee376f3b3ebaf89415ef5cd2efb666e06ddae4839358404a628159b631bc62bd840282ba78c629b18f8e066c0d39da37e15eab8639e277da1d0f67c9e173e7e7c2d9f17808efe3e4f6bb4872ecb2373ecc8ee1110e4d02825820e8c03a03c0b2ddbea4195caf39f41e669f7d251ecf221fbb2f275c0a5d7e05d15840432e6a314cf798d60ae0fa2800bc5570a8e3f9c77995793f5d5dfd294677ac83b628b1c1fab48c67e4336843ae6b4b6ac3515208fe2da628d6672c9f115aa609f5f6 \ No newline at end of file diff --git a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json index cd0e18fad..5e631e9fc 100644 --- a/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json +++ b/modules/tx_unpacker/tests/data/20ded0bfef32fc5eefba2c1f43bcd99acc0b1c3284617c3cb355ad0eadccaa6e/context.json @@ -36,7 +36,36 @@ "slotsPerKesPeriod": 129600, "systemStart": "2017-09-23T21:44:51Z", "updateQuorum": 5, - "genDelegs": {} + "genDelegs": { + "ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c": { + "delegate": "d9e5c76ad5ee778960804094a389f0b546b5c2b140a62f8ec43ea54d", + "vrf": "64fa87e8b29a5b7bfbd6795677e3e878c505bc4a3649485d366b50abadec92d7" + }, + "b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497": { + "delegate": "855d6fc1e54274e331e34478eeac8d060b0b90c1f9e8a2b01167c048", + "vrf": "66d5167a1f426bd1adcc8bbf4b88c280d38c148d135cb41e3f5a39f948ad7fcc" + }, + "60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1": { + "delegate": "7f72a1826ae3b279782ab2bc582d0d2958de65bd86b2c4f82d8ba956", + "vrf": "c0546d9aa5740afd569d3c2d9c412595cd60822bb6d9a4e8ce6c43d12bd0f674" + }, + "f7b341c14cd58fca4195a9b278cce1ef402dc0e06deb77e543cd1757": { + "delegate": "69ae12f9e45c0c9122356c8e624b1fbbed6c22a2e3b4358cf0cb5011", + "vrf": "6394a632af51a32768a6f12dac3485d9c0712d0b54e3f389f355385762a478f2" + }, + "162f94554ac8c225383a2248c245659eda870eaa82d0ef25fc7dcd82": { + "delegate": "4485708022839a7b9b8b639a939c85ec0ed6999b5b6dc651b03c43f6", + "vrf": "aba81e764b71006c515986bf7b37a72fbb5554f78e6775f08e384dbd572a4b32" + }, + "2075a095b3c844a29c24317a94a643ab8e22d54a3a3a72a420260af6": { + "delegate": "6535db26347283990a252313a7903a45e3526ec25ddba381c071b25b", + "vrf": "fcaca997b8105bd860876348fc2c6e68b13607f9bbd23515cd2193b555d267af" + }, + "268cfc0b89e910ead22e0ade91493d8212f53f3e2164b2e4bef0819b": { + "delegate": "1d4f2e1fda43070d71bb22a5522f86943c7c18aeb4fa47a362c27e23", + "vrf": "63ef48bc5355f3e7973100c371d6a095251c80ceb40559f4750aa7014a6fb6db" + } + } }, "utxos": [ [ diff --git a/modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/context.json b/modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/context.json index 7685bc3ed..1876f12a3 100644 --- a/modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/context.json +++ b/modules/tx_unpacker/tests/data/a1aaa9c239f17e6feab5767f61457a3e6251cd0bb94a00a5d41847435caaa42a/context.json @@ -36,7 +36,36 @@ "slotsPerKesPeriod": 129600, "systemStart": "2017-09-23T21:44:51Z", "updateQuorum": 5, - "genDelegs": {} + "genDelegs": { + "ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c": { + "delegate": "d9e5c76ad5ee778960804094a389f0b546b5c2b140a62f8ec43ea54d", + "vrf": "64fa87e8b29a5b7bfbd6795677e3e878c505bc4a3649485d366b50abadec92d7" + }, + "b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497": { + "delegate": "855d6fc1e54274e331e34478eeac8d060b0b90c1f9e8a2b01167c048", + "vrf": "66d5167a1f426bd1adcc8bbf4b88c280d38c148d135cb41e3f5a39f948ad7fcc" + }, + "60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1": { + "delegate": "7f72a1826ae3b279782ab2bc582d0d2958de65bd86b2c4f82d8ba956", + "vrf": "c0546d9aa5740afd569d3c2d9c412595cd60822bb6d9a4e8ce6c43d12bd0f674" + }, + "f7b341c14cd58fca4195a9b278cce1ef402dc0e06deb77e543cd1757": { + "delegate": "69ae12f9e45c0c9122356c8e624b1fbbed6c22a2e3b4358cf0cb5011", + "vrf": "6394a632af51a32768a6f12dac3485d9c0712d0b54e3f389f355385762a478f2" + }, + "162f94554ac8c225383a2248c245659eda870eaa82d0ef25fc7dcd82": { + "delegate": "4485708022839a7b9b8b639a939c85ec0ed6999b5b6dc651b03c43f6", + "vrf": "aba81e764b71006c515986bf7b37a72fbb5554f78e6775f08e384dbd572a4b32" + }, + "2075a095b3c844a29c24317a94a643ab8e22d54a3a3a72a420260af6": { + "delegate": "6535db26347283990a252313a7903a45e3526ec25ddba381c071b25b", + "vrf": "fcaca997b8105bd860876348fc2c6e68b13607f9bbd23515cd2193b555d267af" + }, + "268cfc0b89e910ead22e0ade91493d8212f53f3e2164b2e4bef0819b": { + "delegate": "1d4f2e1fda43070d71bb22a5522f86943c7c18aeb4fa47a362c27e23", + "vrf": "63ef48bc5355f3e7973100c371d6a095251c80ceb40559f4750aa7014a6fb6db" + } + } }, "utxos": [ [ diff --git a/modules/tx_unpacker/tests/data/cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f/context.json b/modules/tx_unpacker/tests/data/cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f/context.json deleted file mode 100644 index c5b010c60..000000000 --- a/modules/tx_unpacker/tests/data/cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f/context.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "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 deleted file mode 100644 index 78dfa06da..000000000 --- a/modules/tx_unpacker/tests/data/cd9037018278826d8ee2a80fe233862d0ff20bf61fc9f74543d682828c7cdb9f/tx.cbor +++ /dev/null @@ -1 +0,0 @@ -84a4008182582018ac56ec3b3495a9cab553c1589109c483784d2efeca1bc0c90a9218a1b5ed65010181825839015d8ddb84e4d2595d55c7e719c7810af21073e927b2e3a9d512201764092f2bf6629b431e22b3fe826edf4d0593c155ca3e760df29afc7ed31a00b00f5a021a00028de1031a004bb2d4a10281845820bd33384a2f1de2d86a9cea56d1cffbc47b4b7f1b6c11b148a21939c24922a85658406970a2bd458f527440c6d231be0f6c22140d3ad5c36a134aa536e2baed7fe327f90edab37676bed02813186773f3ab997c2af437802b2886636cc3bdbb38d40d5820981f1fdcd3166d2a8d4d70b613a16ed81c7fcf3ee70565c9dcffb8dad29c104b41a0f5f6 \ No newline at end of file diff --git a/modules/tx_unpacker/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/context.json b/modules/tx_unpacker/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/context.json new file mode 100644 index 000000000..82ad46059 --- /dev/null +++ b/modules/tx_unpacker/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/context.json @@ -0,0 +1,76 @@ +{ + "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": { + "ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c": { + "delegate": "d9e5c76ad5ee778960804094a389f0b546b5c2b140a62f8ec43ea54d", + "vrf": "64fa87e8b29a5b7bfbd6795677e3e878c505bc4a3649485d366b50abadec92d7" + }, + "b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497": { + "delegate": "855d6fc1e54274e331e34478eeac8d060b0b90c1f9e8a2b01167c048", + "vrf": "66d5167a1f426bd1adcc8bbf4b88c280d38c148d135cb41e3f5a39f948ad7fcc" + }, + "60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1": { + "delegate": "7f72a1826ae3b279782ab2bc582d0d2958de65bd86b2c4f82d8ba956", + "vrf": "c0546d9aa5740afd569d3c2d9c412595cd60822bb6d9a4e8ce6c43d12bd0f674" + }, + "f7b341c14cd58fca4195a9b278cce1ef402dc0e06deb77e543cd1757": { + "delegate": "69ae12f9e45c0c9122356c8e624b1fbbed6c22a2e3b4358cf0cb5011", + "vrf": "6394a632af51a32768a6f12dac3485d9c0712d0b54e3f389f355385762a478f2" + }, + "162f94554ac8c225383a2248c245659eda870eaa82d0ef25fc7dcd82": { + "delegate": "4485708022839a7b9b8b639a939c85ec0ed6999b5b6dc651b03c43f6", + "vrf": "aba81e764b71006c515986bf7b37a72fbb5554f78e6775f08e384dbd572a4b32" + }, + "2075a095b3c844a29c24317a94a643ab8e22d54a3a3a72a420260af6": { + "delegate": "6535db26347283990a252313a7903a45e3526ec25ddba381c071b25b", + "vrf": "fcaca997b8105bd860876348fc2c6e68b13607f9bbd23515cd2193b555d267af" + }, + "268cfc0b89e910ead22e0ade91493d8212f53f3e2164b2e4bef0819b": { + "delegate": "1d4f2e1fda43070d71bb22a5522f86943c7c18aeb4fa47a362c27e23", + "vrf": "63ef48bc5355f3e7973100c371d6a095251c80ceb40559f4750aa7014a6fb6db" + } + } + }, + "utxos": [ + [ + ["4a53dd78e0b2fd4f1ad6c95c3a06d35f00665edd627ed84ba7646f3c3b17ed68", 1], + [4720846, 0] + ] + ] +} diff --git a/modules/tx_unpacker/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/invalid_witnesses_utxow.cbor b/modules/tx_unpacker/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/invalid_witnesses_utxow.cbor new file mode 100644 index 000000000..d9419d4f5 --- /dev/null +++ b/modules/tx_unpacker/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/invalid_witnesses_utxow.cbor @@ -0,0 +1 @@ +84a400818258204a53dd78e0b2fd4f1ad6c95c3a06d35f00665edd627ed84ba7646f3c3b17ed6801018282583901cf8e3c2e266c9e8107203c088e27a650a5234b93cb520771ce211285e49bc56d844b5a48c71dabfbdfed54c7fb5f9d92449c60d272fcd1091a16694e008258390121d5a1ccae1b38703b3ec04f269e0f82ccaabf85cfe50bebe4e709da72d26dcb21798a565cd7b5dda3619b2888e4acaec3302a77c449a0921b000000216d68a27d021a00029201031a009a5af8a10081825820850ccba5b64d2ac20b0f6403e9f2e0cb9ed5dfc70636f89745cb7ce129cb1f0358403d3cc7ff1070bbfd50019645d1036a49ad4b845161d4a02587bf8101f4fbc9bee667c1f08e54e6afcd516a21c21756b319873ff3007de4211bda187f2f533b06f5f6 \ No newline at end of file diff --git a/modules/tx_unpacker/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/tx.cbor b/modules/tx_unpacker/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/tx.cbor new file mode 100644 index 000000000..389a619af --- /dev/null +++ b/modules/tx_unpacker/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/tx.cbor @@ -0,0 +1 @@ +84a400818258204a53dd78e0b2fd4f1ad6c95c3a06d35f00665edd627ed84ba7646f3c3b17ed6801018282583901cf8e3c2e266c9e8107203c088e27a650a5234b93cb520771ce211285e49bc56d844b5a48c71dabfbdfed54c7fb5f9d92449c60d272fcd1091a16694e008258390121d5a1ccae1b38703b3ec04f269e0f82ccaabf85cfe50bebe4e709da72d26dcb21798a565cd7b5dda3619b2888e4acaec3302a77c449a0921b000000216d68a27d021a00029201031a009a5af8a10081825820850ccba5b64d2ac20b0f6403e9f2e0cb9ed5dfc70636f89745cb7ce129cb1f0358403d3cc7ff1070bbfd50019645d1036a49ad4b845161d4a02587bf8101f4fbc9bee667c1f08e54e6afcd516a21c21756b319873ff3007de4211bda187f2f533b02f5f6 \ No newline at end of file diff --git a/modules/utxo_state/Cargo.toml b/modules/utxo_state/Cargo.toml index d425bbaa0..a9cadc3e5 100644 --- a/modules/utxo_state/Cargo.toml +++ b/modules/utxo_state/Cargo.toml @@ -10,6 +10,7 @@ license = "Apache-2.0" [dependencies] acropolis_common = { path = "../../common" } +acropolis_codec = { path = "../../codec" } caryatid_sdk = { workspace = true } @@ -25,3 +26,10 @@ tracing = { workspace = true } [lib] path = "src/utxo_state.rs" + +[dev-dependencies] +hex.workspace = true +pallas.workspace = true +serde.workspace = true +serde_json.workspace = true +test-case = "3.3.1" diff --git a/modules/utxo_state/src/state.rs b/modules/utxo_state/src/state.rs index 8e8cfc945..eff103703 100644 --- a/modules/utxo_state/src/state.rs +++ b/modules/utxo_state/src/state.rs @@ -1,10 +1,12 @@ //! Acropolis UTXOState: State storage +use crate::validations; use crate::volatile_index::VolatileIndex; use acropolis_common::messages::Message; +use acropolis_common::validation::ValidationError; use acropolis_common::{ messages::UTXODeltasMessage, params::SECURITY_PARAMETER_K, BlockInfo, BlockStatus, TxOutput, }; -use acropolis_common::{Address, AddressDelta, UTXOValue, UTxOIdentifier, Value, ValueMap}; +use acropolis_common::{Address, AddressDelta, Era, UTXOValue, UTxOIdentifier, Value, ValueMap}; use anyhow::Result; use async_trait::async_trait; use std::collections::HashMap; @@ -328,7 +330,7 @@ impl State { // Temporary map to sum UTxO deltas efficiently let mut address_map: HashMap = HashMap::new(); - for input in &tx.inputs { + for input in &tx.consumes { if let Some(utxo) = self.lookup_utxo(input).await? { // Remove or mark spent self.observe_input(input, block).await?; @@ -341,7 +343,7 @@ impl State { } } - for output in &tx.outputs { + for output in &tx.produces { self.observe_output(output, block).await?; let addr = output.address.clone(); @@ -380,6 +382,79 @@ impl State { } Ok(()) } + + async fn collect_utxos( + &self, + inputs: &[&UTxOIdentifier], + ) -> HashMap { + let mut utxos = HashMap::new(); + for input in inputs.iter().cloned() { + if utxos.contains_key(input) { + continue; + } + if let Ok(Some(utxo)) = self.lookup_utxo(input).await { + utxos.insert(*input, Some(utxo)); + } else { + utxos.insert(*input, None); + } + } + utxos + .into_iter() + .filter_map(|(input, utxo)| utxo.map(|utxo| (input, utxo))) + .collect::>() + } + + pub async fn validate( + &mut self, + block: &BlockInfo, + deltas: &UTXODeltasMessage, + ) -> Result<(), Box> { + let mut bad_transactions = Vec::new(); + + // collect utxos needed for validation + // NOTE: + // Also consider collateral inputs and reference inputs + let all_inputs = deltas + .deltas + .iter() + .flat_map(|tx_deltas| tx_deltas.consumes.iter()) + .collect::>(); + let mut utxos_needed = self.collect_utxos(&all_inputs).await; + + for tx_deltas in deltas.deltas.iter() { + let mut vkey_hashes_needed = tx_deltas.vkey_hashes_needed.clone().unwrap_or_default(); + let mut script_hashes_needed = + tx_deltas.script_hashes_needed.clone().unwrap_or_default(); + let vkey_hashes_provided = tx_deltas.vkey_hashes_provided.clone().unwrap_or_default(); + let script_hashes_provided = + tx_deltas.script_hashes_provided.clone().unwrap_or_default(); + if block.era == Era::Shelley { + if let Err(e) = validations::validate_shelley_tx( + &tx_deltas.consumes, + &mut vkey_hashes_needed, + &mut script_hashes_needed, + &vkey_hashes_provided, + &script_hashes_provided, + &utxos_needed, + ) { + bad_transactions.push((tx_deltas.tx_identifier.tx_index(), *e)); + } + } + + // add this transaction's outputs to the utxos needed + for output in &tx_deltas.produces { + utxos_needed.insert(output.utxo_identifier, output.utxo_value()); + } + } + + if bad_transactions.is_empty() { + Ok(()) + } else { + Err(Box::new(ValidationError::BadTransactions { + bad_transactions, + })) + } + } } /// Internal helper used during `handle` aggregation for summing UTxO deltas. @@ -479,8 +554,8 @@ mod tests { let deltas = UTXODeltasMessage { deltas: vec![TxUTxODeltas { tx_identifier: Default::default(), - inputs: vec![], - outputs: vec![output.clone()], + consumes: vec![], + produces: vec![output.clone()], vkey_hashes_needed: None, script_hashes_needed: None, vkey_hashes_provided: None, @@ -853,8 +928,8 @@ mod tests { let deltas1 = UTXODeltasMessage { deltas: vec![TxUTxODeltas { tx_identifier: Default::default(), - inputs: vec![], - outputs: vec![output.clone()], + consumes: vec![], + produces: vec![output.clone()], vkey_hashes_needed: None, script_hashes_needed: None, vkey_hashes_provided: None, @@ -873,8 +948,8 @@ mod tests { let deltas2 = UTXODeltasMessage { deltas: vec![TxUTxODeltas { tx_identifier: Default::default(), - inputs: vec![input], - outputs: vec![], + consumes: vec![input], + produces: vec![], vkey_hashes_needed: None, script_hashes_needed: None, vkey_hashes_provided: None, diff --git a/modules/utxo_state/src/test_utils.rs b/modules/utxo_state/src/test_utils.rs new file mode 100644 index 000000000..bb7042ed3 --- /dev/null +++ b/modules/utxo_state/src/test_utils.rs @@ -0,0 +1,91 @@ +use std::{collections::HashMap, str::FromStr}; + +use acropolis_common::{ + protocol_params::ShelleyParams, Address, Datum, ReferenceScript, TxHash, UTXOValue, + UTxOIdentifier, Value, +}; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct UTxOValueJson { + pub address: String, + pub value: Value, + pub datum: Option, + pub reference_script: Option, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct TestContextJson { + pub shelley_params: ShelleyParams, + pub utxos: HashMap, +} + +#[derive(Debug)] +pub struct TestContext { + pub shelley_params: ShelleyParams, + pub utxos: HashMap, +} + +impl From for TestContext { + fn from(json: TestContextJson) -> Self { + Self { + shelley_params: json.shelley_params, + utxos: json + .utxos + .iter() + .map(|(k, v)| { + let tx_hash = TxHash::from_str(k.split('#').nth(0).unwrap()).unwrap(); + let tx_index = k.split('#').nth(1).unwrap().parse::().unwrap(); + ( + UTxOIdentifier::new(tx_hash, tx_index), + UTXOValue { + address: Address::from_string(&v.address).unwrap(), + value: v.value.clone(), + datum: v.datum.clone(), + reference_script: v.reference_script.clone(), + }, + ) + }) + .collect(), + } + } +} +#[macro_export] +macro_rules! include_cbor { + ($filepath:expr) => { + hex::decode(include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/data/", + $filepath, + ))) + .expect(concat!("invalid cbor file: ", $filepath)) + }; +} + +#[macro_export] +macro_rules! include_context { + ($filepath:expr) => { + 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() + }; +} + +#[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, "/", "/context.json")), + $crate::include_cbor!(concat!($hash, "/", $variant, ".cbor")), + ) + }; +} diff --git a/modules/utxo_state/src/utxo_state.rs b/modules/utxo_state/src/utxo_state.rs index 9f1a8844f..b354007cb 100644 --- a/modules/utxo_state/src/utxo_state.rs +++ b/modules/utxo_state/src/utxo_state.rs @@ -7,6 +7,7 @@ use acropolis_common::{ StateQueryResponse, StateTransitionMessage, }, queries::utxos::{UTxOStateQuery, UTxOStateQueryResponse, DEFAULT_UTXOS_QUERY_TOPIC}, + validation::ValidationOutcomes, }; use caryatid_sdk::{module, Context, Subscription}; @@ -20,6 +21,9 @@ use tracing::{error, info, info_span, Instrument}; mod state; use state::{ImmutableUTXOStore, State}; +#[cfg(test)] +mod test_utils; + mod address_delta_publisher; mod volatile_index; use address_delta_publisher::AddressDeltaPublisher; @@ -45,6 +49,8 @@ const DEFAULT_UTXO_DELTAS_SUBSCRIBE_TOPIC: (&str, &str) = const DEFAULT_STORE: &str = "memory"; const DEFAULT_SNAPSHOT_SUBSCRIBE_TOPIC: (&str, &str) = ("snapshot-subscribe-topic", "cardano.snapshot"); +const DEFAULT_UTXO_VALIDATION_TOPIC: (&str, &str) = + ("utxo-validation-publish-topic", "cardano.validation.utxo"); /// UTXO state module #[module( @@ -57,16 +63,36 @@ pub struct UTXOState; impl UTXOState { /// Main run function async fn run( + context: Arc>, state: Arc>, mut utxo_deltas_subscription: Box>, + publish_tx_validation_topic: String, ) -> Result<()> { loop { let Ok((_, message)) = utxo_deltas_subscription.read().await else { return Err(anyhow!("Failed to read UTxO deltas subscription error")); }; + // Validate UTxODeltas + // before applying them match message.as_ref() { Message::Cardano((block, CardanoMessage::UTXODeltas(deltas_msg))) => { + let span = info_span!("utxo_state.validate", block = block.number); + async { + let mut state = state.lock().await; + let mut validation_outcomes = ValidationOutcomes::new(); + if let Err(e) = state.validate(block, deltas_msg).await { + validation_outcomes.push(*e); + } + + validation_outcomes + .publish(&context, &publish_tx_validation_topic, block) + .await + .unwrap_or_else(|e| error!("Failed to publish UTxO validation: {e}")); + } + .instrument(span) + .await; + let span = info_span!("utxo_state.handle", block = block.number); async { let mut state = state.lock().await; @@ -114,6 +140,11 @@ impl UTXOState { .get_string(DEFAULT_UTXOS_QUERY_TOPIC.0) .unwrap_or(DEFAULT_UTXOS_QUERY_TOPIC.1.to_string()); + let utxo_validation_publish_topic = config + .get_string(DEFAULT_UTXO_VALIDATION_TOPIC.0) + .unwrap_or(DEFAULT_UTXO_VALIDATION_TOPIC.1.to_string()); + info!("Creating UTxO validation publisher on '{utxo_validation_publish_topic}'"); + // Create store let store_type = config.get_string("store").unwrap_or(DEFAULT_STORE.to_string()); let store: Arc = match store_type.as_str() { @@ -139,10 +170,16 @@ impl UTXOState { let utxo_deltas_subscription = context.subscribe(&utxo_deltas_subscribe_topic).await?; let state_run = state.clone(); + let context_run = context.clone(); context.run(async move { - Self::run(state_run, utxo_deltas_subscription) - .await - .unwrap_or_else(|e| error!("Failed: {e}")); + Self::run( + context_run, + state_run, + utxo_deltas_subscription, + utxo_validation_publish_topic, + ) + .await + .unwrap_or_else(|e| error!("Failed: {e}")); }); // Subscribe for snapshot messages diff --git a/modules/utxo_state/src/validations/mod.rs b/modules/utxo_state/src/validations/mod.rs index 21740d110..26a52de6c 100644 --- a/modules/utxo_state/src/validations/mod.rs +++ b/modules/utxo_state/src/validations/mod.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use acropolis_common::{ validation::{Phase1ValidationError, TransactionValidationError}, @@ -7,19 +7,15 @@ use acropolis_common::{ use anyhow::Result; mod shelley; -#[allow(dead_code)] -pub fn validate_shelley_tx( +pub fn validate_shelley_tx( inputs: &[UTxOIdentifier], vkey_hashes_needed: &mut HashSet, script_hashes_needed: &mut HashSet, vkey_hashes_provided: &[KeyHash], script_hashes_provided: &[ScriptHash], - lookup_utxo: F, -) -> Result<(), Box> -where - F: Fn(&UTxOIdentifier) -> Result>, -{ - shelley::utxo::validate(inputs, &lookup_utxo) + utxos_needed: &HashMap, +) -> Result<(), Box> { + shelley::utxo::validate(inputs, utxos_needed) .map_err(|e| Box::new((Phase1ValidationError::UTxOValidationError(*e)).into()))?; shelley::utxow::validate( inputs, @@ -27,7 +23,7 @@ where script_hashes_needed, vkey_hashes_provided, script_hashes_provided, - &lookup_utxo, + utxos_needed, ) .map_err(|e| Box::new((Phase1ValidationError::UTxOWValidationError(*e)).into()))?; diff --git a/modules/utxo_state/src/validations/shelley/utxo.rs b/modules/utxo_state/src/validations/shelley/utxo.rs index ac31eccdc..d530ca99b 100644 --- a/modules/utxo_state/src/validations/shelley/utxo.rs +++ b/modules/utxo_state/src/validations/shelley/utxo.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use acropolis_common::{validation::UTxOValidationError, UTXOValue, UTxOIdentifier}; use anyhow::Result; @@ -7,15 +9,12 @@ pub type UTxOValidationResult = Result<(), Box>; /// This prevents double spending. /// Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxo.hs#L468 #[allow(dead_code)] -pub fn validate_bad_inputs_utxo( +pub fn validate_bad_inputs_utxo( inputs: &[UTxOIdentifier], - lookup_utxo: F, -) -> UTxOValidationResult -where - F: Fn(&UTxOIdentifier) -> Result>, -{ + utxos_needed: &HashMap, +) -> UTxOValidationResult { for (index, input) in inputs.iter().enumerate() { - if let Ok(Some(_)) = lookup_utxo(input) { + if utxos_needed.contains_key(input) { continue; } else { return Err(Box::new(UTxOValidationError::BadInputsUTxO { @@ -28,10 +27,39 @@ where } #[allow(dead_code)] -pub fn validate(inputs: &[UTxOIdentifier], lookup_utxo: F) -> UTxOValidationResult -where - F: Fn(&UTxOIdentifier) -> Result>, -{ - validate_bad_inputs_utxo(inputs, lookup_utxo)?; +pub fn validate( + inputs: &[UTxOIdentifier], + utxos_needed: &HashMap, +) -> UTxOValidationResult { + validate_bad_inputs_utxo(inputs, utxos_needed)?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_utils::TestContext, validation_fixture}; + use acropolis_common::{TxHash, UTxOIdentifier}; + use pallas::ledger::traverse::{Era as PallasEra, MultiEraTx}; + use std::str::FromStr; + use test_case::test_case; + + #[test_case(validation_fixture!("da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b") => + matches Ok(()); + "valid transaction 1" + )] + #[test_case(validation_fixture!("da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b", "bad_inputs_utxo") => + matches Err(UTxOValidationError::BadInputsUTxO { bad_input, bad_input_index }) + if bad_input == UTxOIdentifier::new( + TxHash::from_str("e7075bff082ee708dfe49a366717dd4c6d51e9b3a7e5a070dcee253affda0999").unwrap(), 1) + && bad_input_index == 0; + "bad_inputs_utxo" + )] + #[allow(clippy::result_large_err)] + fn shelley_test((ctx, raw_tx): (TestContext, Vec)) -> Result<(), UTxOValidationError> { + let tx = MultiEraTx::decode_for_era(PallasEra::Shelley, &raw_tx).unwrap(); + let tx_inputs = acropolis_codec::map_transaction_inputs(&tx.consumes()); + + validate(&tx_inputs, &ctx.utxos).map_err(|e| *e) + } +} diff --git a/modules/utxo_state/src/validations/shelley/utxow.rs b/modules/utxo_state/src/validations/shelley/utxow.rs index f4f76c771..70577faf0 100644 --- a/modules/utxo_state/src/validations/shelley/utxow.rs +++ b/modules/utxo_state/src/validations/shelley/utxow.rs @@ -1,24 +1,21 @@ //! Shelley era UTxOW Rules //! Reference: https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/eras/shelley/impl/src/Cardano/Ledger/Shelley/Rules/Utxow.hs#L278 -use std::collections::HashSet; - use acropolis_common::{ validation::UTxOWValidationError, KeyHash, ScriptHash, ShelleyAddressPaymentPart, UTXOValue, UTxOIdentifier, }; use anyhow::Result; +use std::collections::{HashMap, HashSet}; -fn get_vkey_script_needed_from_inputs( +fn get_vkey_script_needed_from_inputs( inputs: &[UTxOIdentifier], vkey_hashes_needed: &mut HashSet, script_hashes_needed: &mut HashSet, - lookup_utxo: F, -) where - F: Fn(&UTxOIdentifier) -> Result>, -{ + utxos_needed: &HashMap, +) { // for each UTxO, extract the needed vkey and script hashes for utxo in inputs.iter() { - if let Ok(Some(utxo)) = lookup_utxo(utxo) { + if let Some(utxo) = utxos_needed.get(utxo) { // NOTE: // Need to check inputs from byron bootstrap addresses // with bootstrap witnesses @@ -86,7 +83,7 @@ pub fn validate_needed_witnesses( Ok(()) } -pub fn validate( +pub fn validate( inputs: &[UTxOIdentifier], // Need to include vkey hashes and script hashes // from inputs @@ -94,17 +91,14 @@ pub fn validate( script_hashes_needed: &mut HashSet, vkey_hashes_provided: &[KeyHash], script_hashes_provided: &[ScriptHash], - lookup_utxo: F, -) -> Result<(), Box> -where - F: Fn(&UTxOIdentifier) -> Result>, -{ + utxos_needed: &HashMap, +) -> Result<(), Box> { // Extract vkey hashes and script hashes from inputs get_vkey_script_needed_from_inputs( inputs, vkey_hashes_needed, script_hashes_needed, - lookup_utxo, + utxos_needed, ); // validate missing & extra scripts @@ -115,3 +109,102 @@ where Ok(()) } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + use crate::{test_utils::TestContext, validation_fixture}; + use acropolis_common::{ + AlonzoBabbageUpdateProposal, Era, GenesisDelegates, NetworkId, Transaction, + TxCertificateWithPos, TxIdentifier, Withdrawal, + }; + use pallas::ledger::traverse::{Era as PallasEra, MultiEraTx}; + use test_case::test_case; + + fn get_vkey_script_needed( + certs: &[TxCertificateWithPos], + withdrawals: &[Withdrawal], + proposal_update: &Option, + genesis_delgs: &GenesisDelegates, + vkey_hashes: &mut HashSet, + script_hashes: &mut HashSet, + ) { + // for each certificate, get the required vkey and script hashes + for cert_with_pos in certs.iter() { + cert_with_pos.cert.get_cert_authors(vkey_hashes, script_hashes); + } + + // for each withdrawal, get the required vkey and script hashes + for withdrawal in withdrawals.iter() { + withdrawal.get_withdrawal_authors(vkey_hashes, script_hashes); + } + + // for each governance action, get the required vkey hashes + if let Some(proposal_update) = proposal_update.as_ref() { + proposal_update.get_governance_authors(vkey_hashes, genesis_delgs); + } + } + + #[test_case(validation_fixture!("da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b") => + matches Ok(()); + "valid transaction 1" + )] + #[test_case(validation_fixture!("b516588da34b58b7d32b6a057f513e16ea8c87de46615631be3316d8a8847d46") => + matches Ok(()); + "valid transaction 2 - with protocol update" + )] + #[test_case(validation_fixture!("da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b", "missing_vkey_witnesses_utxow") => + matches Err(UTxOWValidationError::MissingVKeyWitnessesUTxOW { key_hash }) + if key_hash == KeyHash::from_str("b0baefb8dedefd7ec935514696ea5a66e9520f31dc8867737f0f0084").unwrap(); + "missing_vkey_witnesses_utxow" + )] + #[allow(clippy::result_large_err)] + fn shelley_test((ctx, raw_tx): (TestContext, Vec)) -> Result<(), UTxOWValidationError> { + let tx = MultiEraTx::decode_for_era(PallasEra::Shelley, &raw_tx).unwrap(); + let raw_tx = tx.encode(); + let tx_identifier = TxIdentifier::new(4533644, 1); + let Transaction { + consumes: tx_consumes, + certs: tx_certs, + withdrawals: tx_withdrawals, + proposal_update: tx_proposal_update, + vkey_witnesses, + native_scripts, + error: tx_error, + .. + } = acropolis_codec::map_transaction( + &tx, + &raw_tx, + tx_identifier, + NetworkId::Mainnet, + Era::Shelley, + ); + assert!(tx_error.is_none()); + + let mut vkey_hashes_needed = HashSet::new(); + let mut script_hashes_needed = HashSet::new(); + get_vkey_script_needed( + &tx_certs, + &tx_withdrawals, + &tx_proposal_update, + &ctx.shelley_params.gen_delegs, + &mut vkey_hashes_needed, + &mut script_hashes_needed, + ); + let vkey_hashes_provided = vkey_witnesses.iter().map(|w| w.key_hash()).collect::>(); + let script_hashes_provided = + native_scripts.iter().map(|s| s.compute_hash()).collect::>(); + + validate( + &tx_consumes, + &mut vkey_hashes_needed, + &mut script_hashes_needed, + &vkey_hashes_provided, + &script_hashes_provided, + &ctx.utxos, + ) + .map_err(|e| *e) + } +} diff --git a/modules/utxo_state/tests/data/b516588da34b58b7d32b6a057f513e16ea8c87de46615631be3316d8a8847d46/context.json b/modules/utxo_state/tests/data/b516588da34b58b7d32b6a057f513e16ea8c87de46615631be3316d8a8847d46/context.json new file mode 100644 index 000000000..1249ddd92 --- /dev/null +++ b/modules/utxo_state/tests/data/b516588da34b58b7d32b6a057f513e16ea8c87de46615631be3316d8a8847d46/context.json @@ -0,0 +1,80 @@ +{ + "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": { + "ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c": { + "delegate": "d9e5c76ad5ee778960804094a389f0b546b5c2b140a62f8ec43ea54d", + "vrf": "64fa87e8b29a5b7bfbd6795677e3e878c505bc4a3649485d366b50abadec92d7" + }, + "b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497": { + "delegate": "855d6fc1e54274e331e34478eeac8d060b0b90c1f9e8a2b01167c048", + "vrf": "66d5167a1f426bd1adcc8bbf4b88c280d38c148d135cb41e3f5a39f948ad7fcc" + }, + "60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1": { + "delegate": "7f72a1826ae3b279782ab2bc582d0d2958de65bd86b2c4f82d8ba956", + "vrf": "c0546d9aa5740afd569d3c2d9c412595cd60822bb6d9a4e8ce6c43d12bd0f674" + }, + "f7b341c14cd58fca4195a9b278cce1ef402dc0e06deb77e543cd1757": { + "delegate": "69ae12f9e45c0c9122356c8e624b1fbbed6c22a2e3b4358cf0cb5011", + "vrf": "6394a632af51a32768a6f12dac3485d9c0712d0b54e3f389f355385762a478f2" + }, + "162f94554ac8c225383a2248c245659eda870eaa82d0ef25fc7dcd82": { + "delegate": "4485708022839a7b9b8b639a939c85ec0ed6999b5b6dc651b03c43f6", + "vrf": "aba81e764b71006c515986bf7b37a72fbb5554f78e6775f08e384dbd572a4b32" + }, + "2075a095b3c844a29c24317a94a643ab8e22d54a3a3a72a420260af6": { + "delegate": "6535db26347283990a252313a7903a45e3526ec25ddba381c071b25b", + "vrf": "fcaca997b8105bd860876348fc2c6e68b13607f9bbd23515cd2193b555d267af" + }, + "268cfc0b89e910ead22e0ade91493d8212f53f3e2164b2e4bef0819b": { + "delegate": "1d4f2e1fda43070d71bb22a5522f86943c7c18aeb4fa47a362c27e23", + "vrf": "63ef48bc5355f3e7973100c371d6a095251c80ceb40559f4750aa7014a6fb6db" + } + } + }, + "utxos": { + "5dd12a1c34f74d150f987ea8a03507b64b35ef4ec1f61b648e9836c1946cad35#0": { + "address": "addr1v8vqle5aa50ljr6pu5ndqve29luch29qmpwwhz2pk5tcggqn3q8mu", + "value": { + "lovelace": 5000000, + "assets": [] + }, + "datum": null, + "reference_script": null + } + } +} diff --git a/modules/utxo_state/tests/data/b516588da34b58b7d32b6a057f513e16ea8c87de46615631be3316d8a8847d46/tx.cbor b/modules/utxo_state/tests/data/b516588da34b58b7d32b6a057f513e16ea8c87de46615631be3316d8a8847d46/tx.cbor new file mode 100644 index 000000000..b6f6195b4 --- /dev/null +++ b/modules/utxo_state/tests/data/b516588da34b58b7d32b6a057f513e16ea8c87de46615631be3316d8a8847d46/tx.cbor @@ -0,0 +1 @@ +84a500818258205dd12a1c34f74d150f987ea8a03507b64b35ef4ec1f61b648e9836c1946cad35000181825839016445bb08465c26cf435a4274b8dadff753b3b9a87f4aa8f88479431d4eb646b86f18f6f4266684f37fbcf4027c650747dc45278ab0bf8c8e1a00491ee7021a00032c59031a0051c0d50682a7581c162f94554ac8c225383a2248c245659eda870eaa82d0ef25fc7dcd82a10cd81e82090a581c2075a095b3c844a29c24317a94a643ab8e22d54a3a3a72a420260af6a10cd81e82090a581c268cfc0b89e910ead22e0ade91493d8212f53f3e2164b2e4bef0819ba10cd81e82090a581c60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1a10cd81e82090a581cad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950ca10cd81e82090a581cb9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497a10cd81e82090a581cf7b341c14cd58fca4195a9b278cce1ef402dc0e06deb77e543cd1757a10cd81e82090a18d2a1008882582061261a95b7613ee6bf2067dad77b70349729b0c50d57bc1cf30de0db4a1e73a8584023088022bfda11224d7ab6d4af7bba71255b2bdb7aeb2a2f00a6e9bef18a77675f1e2d0a9b980cda22887ed90f4ccbaf7fcb1c0df305286217cacb465b080e048258209180d818e69cd997e34663c418a648c076f2e19cd4194e486e159d8580bc6cda5840932fe01ffdd7173e8a6fc29aafc17db1f79ffd5e13bc2d587b697c8233c66279323f3de4454640520b676bcec7097c8b08309634c59b46f5f68680fd2fb4300c82582089c29f8c4af27b7accbe589747820134ebbaa1caf3ce949270a3d0c7dcfd541b58407031acf9d1348112719375d398c0b5855075ce112e619daf64f07259078217c581dbb8237d5401a3c9be0d3db94834779713d52f0291a01cf7c2af0d58dd280e825820f14f712dc600d793052d4842d50cefa4e65884ea6cf83707079eb8ce302efc8558403c4152ec42df686070a3a8968ad7f843d54d7ab2b266566ef35044252fb1adec753c5e48404514a417fddbbed468ea272abaa1bad424029d25041191222e450f8258208b53207629f9a30e4b2015044f337c01735abe67243c19470c9dae8c7b732798584051cbf9e97cdd478e45404930bdc1ecf968118e502e3336ef89d0b5edfa3c144e6a01d0208f2417d5f04a1126aa7cf541021a50d74cca908513e760933713900c8258205fddeedade2714d6db2f9e1104743d2d8d818ecddc306e176108db14caadd4415840fc5fd251802960f4941f90d37fb083a660f49d93cc5e89dd2f1512f48df367c2865e4217a90bbd8cbd5668b6bcd15db2ab142c977176013a2bf594b9831a590b825820cbc6b506e94fbefe442eecee376f3b3ebaf89415ef5cd2efb666e06ddae48393584044600676db9fa536cc54ef7fe4c2b19ecd0fb8262d922ec9302c82463ec8748e0ae538754a0ba2c09e6035aeb877ac353dbc355330991286c14c3e5e0c43bb09825820e8c03a03c0b2ddbea4195caf39f41e669f7d251ecf221fbb2f275c0a5d7e05d15840c60620d58a62835c79d23d87cf5c5b140e1a928cb38989168974c0517322e0602681a1f4260836e17cee6b1a60ada3b385fc3b4ecd34e6f24ff16ec6f41ecf08f5f6 \ No newline at end of file diff --git a/modules/utxo_state/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/bad_inputs_utxo.cbor b/modules/utxo_state/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/bad_inputs_utxo.cbor new file mode 100644 index 000000000..ab87dffe5 --- /dev/null +++ b/modules/utxo_state/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/bad_inputs_utxo.cbor @@ -0,0 +1 @@ +84a40081825820e7075bff082ee708dfe49a366717dd4c6d51e9b3a7e5a070dcee253affda099901018282583901cf8e3c2e266c9e8107203c088e27a650a5234b93cb520771ce211285e49bc56d844b5a48c71dabfbdfed54c7fb5f9d92449c60d272fcd1091a16694e008258390121d5a1ccae1b38703b3ec04f269e0f82ccaabf85cfe50bebe4e709da72d26dcb21798a565cd7b5dda3619b2888e4acaec3302a77c449a0921b000000216d68a27d021a00029201031a009a5af8a10081825820850ccba5b64d2ac20b0f6403e9f2e0cb9ed5dfc70636f89745cb7ce129cb1f0358403d3cc7ff1070bbfd50019645d1036a49ad4b845161d4a02587bf8101f4fbc9bee667c1f08e54e6afcd516a21c21756b319873ff3007de4211bda187f2f533b02f5f6 \ No newline at end of file diff --git a/modules/utxo_state/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/context.json b/modules/utxo_state/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/context.json new file mode 100644 index 000000000..7ca166b99 --- /dev/null +++ b/modules/utxo_state/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/context.json @@ -0,0 +1,80 @@ +{ + "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": { + "ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c": { + "delegate": "d9e5c76ad5ee778960804094a389f0b546b5c2b140a62f8ec43ea54d", + "vrf": "64fa87e8b29a5b7bfbd6795677e3e878c505bc4a3649485d366b50abadec92d7" + }, + "b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497": { + "delegate": "855d6fc1e54274e331e34478eeac8d060b0b90c1f9e8a2b01167c048", + "vrf": "66d5167a1f426bd1adcc8bbf4b88c280d38c148d135cb41e3f5a39f948ad7fcc" + }, + "60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1": { + "delegate": "7f72a1826ae3b279782ab2bc582d0d2958de65bd86b2c4f82d8ba956", + "vrf": "c0546d9aa5740afd569d3c2d9c412595cd60822bb6d9a4e8ce6c43d12bd0f674" + }, + "f7b341c14cd58fca4195a9b278cce1ef402dc0e06deb77e543cd1757": { + "delegate": "69ae12f9e45c0c9122356c8e624b1fbbed6c22a2e3b4358cf0cb5011", + "vrf": "6394a632af51a32768a6f12dac3485d9c0712d0b54e3f389f355385762a478f2" + }, + "162f94554ac8c225383a2248c245659eda870eaa82d0ef25fc7dcd82": { + "delegate": "4485708022839a7b9b8b639a939c85ec0ed6999b5b6dc651b03c43f6", + "vrf": "aba81e764b71006c515986bf7b37a72fbb5554f78e6775f08e384dbd572a4b32" + }, + "2075a095b3c844a29c24317a94a643ab8e22d54a3a3a72a420260af6": { + "delegate": "6535db26347283990a252313a7903a45e3526ec25ddba381c071b25b", + "vrf": "fcaca997b8105bd860876348fc2c6e68b13607f9bbd23515cd2193b555d267af" + }, + "268cfc0b89e910ead22e0ade91493d8212f53f3e2164b2e4bef0819b": { + "delegate": "1d4f2e1fda43070d71bb22a5522f86943c7c18aeb4fa47a362c27e23", + "vrf": "63ef48bc5355f3e7973100c371d6a095251c80ceb40559f4750aa7014a6fb6db" + } + } + }, + "utxos": { + "4a53dd78e0b2fd4f1ad6c95c3a06d35f00665edd627ed84ba7646f3c3b17ed68#1": { + "address": "addr1qxct4macmm006lkfx4g5d9h2tfnwj5s0x8wgsemn0u8spprj6fkukgte3ft9e4a4mk3krxeg3rj2etkrxq4803zf5zfq4ex956", + "value": { + "lovelace": 143945663102, + "assets": [] + }, + "datum": null, + "reference_script": null + } + } +} diff --git a/modules/utxo_state/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/missing_vkey_witnesses_utxow.cbor b/modules/utxo_state/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/missing_vkey_witnesses_utxow.cbor new file mode 100644 index 000000000..152600b9e --- /dev/null +++ b/modules/utxo_state/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/missing_vkey_witnesses_utxow.cbor @@ -0,0 +1 @@ +84A400818258204A53DD78E0B2FD4F1AD6C95C3A06D35F00665EDD627ED84BA7646F3C3B17ED6801018282583901CF8E3C2E266C9E8107203C088E27A650A5234B93CB520771CE211285E49BC56D844B5A48C71DABFBDFED54C7FB5F9D92449C60D272FCD1091A16694E008258390121D5A1CCAE1B38703B3EC04F269E0F82CCAABF85CFE50BEBE4E709DA72D26DCB21798A565CD7B5DDA3619B2888E4ACAEC3302A77C449A0921B000000216D68A27D021A00029201031A009A5AF8A10080F5F6 \ No newline at end of file diff --git a/modules/utxo_state/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/tx.cbor b/modules/utxo_state/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/tx.cbor new file mode 100644 index 000000000..389a619af --- /dev/null +++ b/modules/utxo_state/tests/data/da350a9e2a14717172cee9e37df02b14b5718ea1934ce6bea25d739d9226f01b/tx.cbor @@ -0,0 +1 @@ +84a400818258204a53dd78e0b2fd4f1ad6c95c3a06d35f00665edd627ed84ba7646f3c3b17ed6801018282583901cf8e3c2e266c9e8107203c088e27a650a5234b93cb520771ce211285e49bc56d844b5a48c71dabfbdfed54c7fb5f9d92449c60d272fcd1091a16694e008258390121d5a1ccae1b38703b3ec04f269e0f82ccaabf85cfe50bebe4e709da72d26dcb21798a565cd7b5dda3619b2888e4acaec3302a77c449a0921b000000216d68a27d021a00029201031a009a5af8a10081825820850ccba5b64d2ac20b0f6403e9f2e0cb9ed5dfc70636f89745cb7ce129cb1f0358403d3cc7ff1070bbfd50019645d1036a49ad4b845161d4a02587bf8101f4fbc9bee667c1f08e54e6afcd516a21c21756b319873ff3007de4211bda187f2f533b02f5f6 \ No newline at end of file