diff --git a/pallas-applying/docs/alonzo.md b/pallas-applying/docs/alonzo.md index 273e213a4..9850ab05f 100644 --- a/pallas-applying/docs/alonzo.md +++ b/pallas-applying/docs/alonzo.md @@ -176,7 +176,7 @@ Let ***tx ∈ Tx*** be one of its Alonzo transactions, with transaction body *** - **Each datum hash in a Plutus script input matches the hash of a datum in the transaction witness set**: {h : (a,\_,h) ∈ txIns(txBody) ◁ utxo, isPlutusScriptAddress(txWits, a)} ⊆ {datumHash(d) : d ∈ txDats(txWits)} - - **Each datum object in the transaction witness set corresponds either to an output datum hash or to the datum hash of a Plutus script input**: + - **Each datum object in the transaction witness set corresponds either to a Plutus script input datum hash or to an output datum hash**: {datumHash(d): d ∈ txDats(txWits)} ⊆ {h: (a,\_,h) ∈ txIns(txBody) ◁ utxo, isPlutusScriptAddress(txWits, a)} ∪ {h: (\_,\_,h) ∈ txOuts(txBody)} - **The set of redeemers in the transaction witness set should match the set of Plutus scripts needed to validate the transaction**: diff --git a/pallas-applying/src/alonzo.rs b/pallas-applying/src/alonzo.rs index eb04e388f..7a5517e5f 100644 --- a/pallas-applying/src/alonzo.rs +++ b/pallas-applying/src/alonzo.rs @@ -2,7 +2,8 @@ use crate::utils::{ add_minted_value, add_values, empty_value, get_alonzo_comp_tx_size, - get_lovelace_from_alonzo_value, get_network_id_value, values_are_equal, verify_signature, + get_lovelace_from_alonzo_value, get_network_id_value, mk_alonzo_vk_wits_check_list, + values_are_equal, verify_signature, AlonzoError::*, AlonzoProtParams, FeePolicy, UTxOs, ValidationError::{self, *}, @@ -17,7 +18,7 @@ use pallas_primitives::{ }, byron::TxOut, }; -use pallas_traverse::{ComputeHash, MultiEraInput, MultiEraOutput}; +use pallas_traverse::{MultiEraInput, MultiEraOutput, OriginalHash}; pub fn validate_alonzo_tx( mtx: &MintedTx, @@ -38,7 +39,7 @@ pub fn validate_alonzo_tx( check_network_id(tx_body, network_id)?; check_tx_size(size, prot_pps)?; check_tx_ex_units(mtx, prot_pps)?; - check_witnesses(tx_body, utxos, mtx)?; + check_witness_set(mtx, utxos)?; check_languages(mtx, prot_pps)?; check_metadata(tx_body, mtx)?; check_script_data_hash(tx_body, mtx, prot_pps)?; @@ -392,10 +393,11 @@ fn check_tx_ex_units(mtx: &MintedTx, prot_pps: &AlonzoProtParams) -> ValidationR Ok(()) } -fn check_witnesses(tx_body: &TransactionBody, utxos: &UTxOs, mtx: &MintedTx) -> ValidationResult { +fn check_witness_set(mtx: &MintedTx, utxos: &UTxOs) -> ValidationResult { + let tx_hash: &Vec = &Vec::from(mtx.transaction_body.original_hash().as_ref()); + let tx_body: &TransactionBody = &mtx.transaction_body; let tx_wits: &MintedWitnessSet = &mtx.transaction_witness_set; let vkey_wits: &Option> = &tx_wits.vkeywitness; - let tx_hash: &Vec = &Vec::from(tx_body.compute_hash().as_ref()); check_needed_scripts_are_included( tx_body, utxos, @@ -404,8 +406,8 @@ fn check_witnesses(tx_body: &TransactionBody, utxos: &UTxOs, mtx: &MintedTx) -> )?; check_datums(tx_body, utxos, &tx_wits.plutus_data)?; check_redeemers(tx_body, utxos, tx_wits)?; - check_vkey_input_wits(tx_body, utxos, &tx_wits.vkeywitness)?; - check_required_signers(&tx_body.required_signers, vkey_wits, tx_hash) + check_required_signers(&tx_body.required_signers, vkey_wits, tx_hash)?; + check_vkey_input_wits(mtx, &tx_wits.vkeywitness, utxos) } // The set of needed scripts (minting policies, native scripts and Plutus @@ -460,9 +462,59 @@ fn check_redeemers( // The owner of each transaction input and each collateral input should have // signed the transaction. fn check_vkey_input_wits( - _tx_body: &TransactionBody, - _utxos: &UTxOs, - _vkey_wits: &Option>, + mtx: &MintedTx, + vkey_wits: &Option>, + utxos: &UTxOs, +) -> ValidationResult { + let tx_body: &TransactionBody = &mtx.transaction_body; + let vk_wits: &mut Vec<(bool, VKeyWitness)> = + &mut mk_alonzo_vk_wits_check_list(vkey_wits, Alonzo(MissingVKWitness))?; + let tx_hash: &Vec = &Vec::from(mtx.transaction_body.original_hash().as_ref()); + let mut inputs_and_collaterals: Vec = Vec::new(); + inputs_and_collaterals.extend(tx_body.inputs.clone()); + match &tx_body.collateral { + Some(collaterals) => inputs_and_collaterals.extend(collaterals.clone()), + None => (), + } + for input in inputs_and_collaterals.iter() { + match utxos.get(&MultiEraInput::from_alonzo_compatible(input)) { + Some(multi_era_output) => { + if let Some(alonzo_comp_output) = MultiEraOutput::as_alonzo(multi_era_output) { + match get_payment_part(alonzo_comp_output)? { + ShelleyPaymentPart::Key(payment_key_hash) => { + check_vk_wit(&payment_key_hash, vk_wits, tx_hash)? + } + ShelleyPaymentPart::Script(_) => (), + } + } + } + None => return Err(Alonzo(InputNotInUTxO)), + } + } + check_remaining_vk_wits(vk_wits, tx_hash) +} + +fn check_vk_wit( + payment_key_hash: &AddrKeyhash, + wits: &mut Vec<(bool, VKeyWitness)>, + data_to_verify: &Vec, +) -> ValidationResult { + for (found, vkey_wit) in wits { + if pallas_crypto::hash::Hasher::<224>::hash(&vkey_wit.vkey.clone()) == *payment_key_hash { + if !verify_signature(vkey_wit, data_to_verify) { + return Err(Alonzo(VKWrongSignature)); + } else { + *found = true; + return Ok(()); + } + } + } + Err(Alonzo(MissingVKWitness)) +} + +fn check_remaining_vk_wits( + _vk_wits: &mut [(bool, VKeyWitness)], + _data_to_verify: &[u8], ) -> ValidationResult { Ok(()) } @@ -472,13 +524,13 @@ fn check_vkey_input_wits( fn check_required_signers( required_signers: &Option, vkey_wits: &Option>, - tx_hash: &Vec, + data_to_verify: &Vec, ) -> ValidationResult { if let Some(req_signers) = &required_signers { match &vkey_wits { Some(vkey_wits) => { for req_signer in req_signers { - check_required_signer(req_signer, vkey_wits, tx_hash)? + find_and_check_req_signer(req_signer, vkey_wits, data_to_verify)? } } None => return Err(Alonzo(MissingReqSigner)), @@ -488,16 +540,14 @@ fn check_required_signers( } // Try to find the verification key in the witnesses, and verify the signature. -fn check_required_signer( - req_signer: &AddrKeyhash, +fn find_and_check_req_signer( + vkey_hash: &AddrKeyhash, vkey_wits: &Vec, - tx_hash: &Vec, + data_to_verify: &Vec, ) -> ValidationResult { for vkey_wit in vkey_wits { - if pallas_crypto::hash::Hasher::<224>::hash(&Vec::::from(vkey_wit.vkey.clone())) - == *req_signer - { - if !verify_signature(vkey_wit, tx_hash) { + if pallas_crypto::hash::Hasher::<224>::hash(&vkey_wit.vkey.clone()) == *vkey_hash { + if !verify_signature(vkey_wit, data_to_verify) { return Err(Alonzo(ReqSignerWrongSig)); } else { return Ok(()); diff --git a/pallas-applying/src/shelley_ma.rs b/pallas-applying/src/shelley_ma.rs index 705fcc59c..d5e0e6a1d 100644 --- a/pallas-applying/src/shelley_ma.rs +++ b/pallas-applying/src/shelley_ma.rs @@ -32,6 +32,7 @@ pub fn validate_shelley_ma_tx( era: &Era, ) -> ValidationResult { let tx_body: &TransactionBody = &mtx.transaction_body; + let tx_wits: &MintedWitnessSet = &mtx.transaction_witness_set; let size: &u64 = &get_alonzo_comp_tx_size(tx_body).ok_or(ShelleyMA(UnknownTxSize))?; check_ins_not_empty(tx_body)?; check_ins_in_utxos(tx_body, utxos)?; @@ -42,7 +43,7 @@ pub fn validate_shelley_ma_tx( check_fees(tx_body, size, prot_pps)?; check_network_id(tx_body, network_id)?; check_metadata(tx_body, mtx)?; - check_witnesses(tx_body, utxos, mtx)?; + check_witnesses(tx_body, tx_wits, utxos)?; check_minting(tx_body, mtx) } @@ -214,8 +215,11 @@ fn check_metadata(tx_body: &TransactionBody, mtx: &MintedTx) -> ValidationResult } } -fn check_witnesses(tx_body: &TransactionBody, utxos: &UTxOs, mtx: &MintedTx) -> ValidationResult { - let tx_wits: &MintedWitnessSet = &mtx.transaction_witness_set; +fn check_witnesses( + tx_body: &TransactionBody, + tx_wits: &MintedWitnessSet, + utxos: &UTxOs, +) -> ValidationResult { let vk_wits: &mut Vec<(bool, VKeyWitness)> = &mut mk_alonzo_vk_wits_check_list(&tx_wits.vkeywitness, ShelleyMA(MissingVKWitness))?; let tx_hash: &Vec = &Vec::from(tx_body.compute_hash().as_ref()); diff --git a/pallas-applying/src/utils/validation.rs b/pallas-applying/src/utils/validation.rs index 03ce8433f..1678fb0b8 100644 --- a/pallas-applying/src/utils/validation.rs +++ b/pallas-applying/src/utils/validation.rs @@ -74,6 +74,8 @@ pub enum AlonzoError { RedeemerMissing, TxExUnitsExceeded, MaxTxSizeExceeded, + MissingVKWitness, + VKWrongSignature, MissingReqSigner, ReqSignerWrongSig, } diff --git a/pallas-applying/tests/alonzo.rs b/pallas-applying/tests/alonzo.rs index f385a3050..0566b6818 100644 --- a/pallas-applying/tests/alonzo.rs +++ b/pallas-applying/tests/alonzo.rs @@ -1,6 +1,7 @@ pub mod common; use common::*; +use hex; use pallas_addresses::{Address, Network, ShelleyAddress, ShelleyPaymentPart}; use pallas_applying::{ utils::{ @@ -16,9 +17,9 @@ use pallas_codec::{ }, utils::{Bytes, KeyValuePairs}, }; -use pallas_crypto::hash::Hash; use pallas_primitives::alonzo::{ - AddrKeyhash, MintedTx, NetworkId, TransactionBody, TransactionOutput, Value, + AddrKeyhash, MintedTx, MintedWitnessSet, NetworkId, TransactionBody, TransactionOutput, + VKeyWitness, Value, }; use pallas_traverse::{Era, MultiEraInput, MultiEraOutput, MultiEraTx}; use std::borrow::Cow; @@ -86,10 +87,9 @@ mod alonzo_tests { Value::Multiasset( 1724100, KeyValuePairs::from(Vec::from([( - Hash::<28>::new([ - 176, 1, 7, 107, 52, 168, 126, 125, 72, 236, 70, 112, 58, 111, 80, - 249, 50, 137, 88, 42, 217, 189, 190, 255, 127, 30, 50, 149, - ]), + "b001076b34a87e7d48ec46703a6f50f93289582ad9bdbeff7f1e3295" + .parse() + .unwrap(), KeyValuePairs::from(Vec::from([( Bytes::from(hex::decode("4879706562656173747332343233").unwrap()), 1, @@ -444,10 +444,9 @@ mod alonzo_tests { Value::Multiasset( 1724100, KeyValuePairs::from(Vec::from([( - Hash::<28>::new([ - 176, 1, 7, 107, 52, 168, 126, 125, 72, 236, 70, 112, 58, 111, 80, - 249, 50, 137, 88, 42, 217, 189, 190, 255, 127, 30, 50, 149, - ]), + "b001076b34a87e7d48ec46703a6f50f93289582ad9bdbeff7f1e3295" + .parse() + .unwrap(), KeyValuePairs::from(Vec::from([( Bytes::from(hex::decode("4879706562656173747332343233").unwrap()), 1, @@ -567,10 +566,9 @@ mod alonzo_tests { Value::Multiasset( 1724100, KeyValuePairs::from(Vec::from([( - Hash::<28>::new([ - 176, 1, 7, 107, 52, 168, 126, 125, 72, 236, 70, 112, 58, 111, 80, - 249, 50, 137, 88, 42, 217, 189, 190, 255, 127, 30, 50, 149, - ]), + "b001076b34a87e7d48ec46703a6f50f93289582ad9bdbeff7f1e3295" + .parse() + .unwrap(), KeyValuePairs::from(Vec::from([( Bytes::from(hex::decode("4879706562656173747332343233").unwrap()), 1, @@ -678,10 +676,9 @@ mod alonzo_tests { Value::Multiasset( 1724100, KeyValuePairs::from(Vec::from([( - Hash::<28>::new([ - 176, 1, 7, 107, 52, 168, 126, 125, 72, 236, 70, 112, 58, 111, 80, - 249, 50, 137, 88, 42, 217, 189, 190, 255, 127, 30, 50, 149, - ]), + "b001076b34a87e7d48ec46703a6f50f93289582ad9bdbeff7f1e3295" + .parse() + .unwrap(), KeyValuePairs::from(Vec::from([( Bytes::from(hex::decode("4879706562656173747332343233").unwrap()), 1, @@ -813,10 +810,9 @@ mod alonzo_tests { Value::Multiasset( 1724100, KeyValuePairs::from(Vec::from([( - Hash::<28>::new([ - 176, 1, 7, 107, 52, 168, 126, 125, 72, 236, 70, 112, 58, 111, 80, - 249, 50, 137, 88, 42, 217, 189, 190, 255, 127, 30, 50, 149, - ]), + "b001076b34a87e7d48ec46703a6f50f93289582ad9bdbeff7f1e3295" + .parse() + .unwrap(), KeyValuePairs::from(Vec::from([( Bytes::from(hex::decode("4879706562656173747332343233").unwrap()), 1, @@ -877,10 +873,9 @@ mod alonzo_tests { Value::Multiasset( 5000000, KeyValuePairs::from(Vec::from([( - Hash::<28>::new([ - 176, 1, 7, 107, 52, 168, 126, 125, 72, 236, 70, 112, 58, 111, 80, 249, - 50, 137, 88, 42, 217, 189, 190, 255, 127, 30, 50, 149, - ]), + "b001076b34a87e7d48ec46703a6f50f93289582ad9bdbeff7f1e3295" + .parse() + .unwrap(), KeyValuePairs::from(Vec::from([( Bytes::from(hex::decode("4879706562656173747332343233").unwrap()), 1000, @@ -937,10 +932,9 @@ mod alonzo_tests { Value::Multiasset( 1724100, KeyValuePairs::from(Vec::from([( - Hash::<28>::new([ - 176, 1, 7, 107, 52, 168, 126, 125, 72, 236, 70, 112, 58, 111, 80, - 249, 50, 137, 88, 42, 217, 189, 190, 255, 127, 30, 50, 149, - ]), + "b001076b34a87e7d48ec46703a6f50f93289582ad9bdbeff7f1e3295" + .parse() + .unwrap(), KeyValuePairs::from(Vec::from([( Bytes::from(hex::decode("4879706562656173747332343233").unwrap()), 1, @@ -1239,10 +1233,9 @@ mod alonzo_tests { Value::Multiasset( 1724100, KeyValuePairs::from(Vec::from([( - Hash::<28>::new([ - 176, 1, 7, 107, 52, 168, 126, 125, 72, 236, 70, 112, 58, 111, 80, - 249, 50, 137, 88, 42, 217, 189, 190, 255, 127, 30, 50, 149, - ]), + "b001076b34a87e7d48ec46703a6f50f93289582ad9bdbeff7f1e3295" + .parse() + .unwrap(), KeyValuePairs::from(Vec::from([( Bytes::from(hex::decode("4879706562656173747332343233").unwrap()), 1, @@ -1400,10 +1393,9 @@ mod alonzo_tests { Value::Multiasset( 1724100, KeyValuePairs::from(Vec::from([( - Hash::<28>::new([ - 176, 1, 7, 107, 52, 168, 126, 125, 72, 236, 70, 112, 58, 111, 80, - 249, 50, 137, 88, 42, 217, 189, 190, 255, 127, 30, 50, 149, - ]), + "b001076b34a87e7d48ec46703a6f50f93289582ad9bdbeff7f1e3295" + .parse() + .unwrap(), KeyValuePairs::from(Vec::from([( Bytes::from(hex::decode("4879706562656173747332343233").unwrap()), 1, @@ -1512,4 +1504,119 @@ mod alonzo_tests { }, } } + + #[test] + // Same as successful_mainnet_tx, except that the list of verification key is + // empty. + fn missing_vk_witness() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/alonzo1.tx")); + let mut mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let utxos: UTxOs = mk_utxo_for_alonzo_compatible_tx( + &mtx.transaction_body, + &[( + String::from(include_str!("../../test_data/alonzo1.address")), + Value::Coin(1549646822), + None, + )], + ); + let mut tx_wits: MintedWitnessSet = mtx.transaction_witness_set.unwrap().clone(); + tx_wits.vkeywitness = Some(vec![]); + let mut tx_buf: Vec = Vec::new(); + match encode(tx_wits, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?})", err), + }; + mtx.transaction_witness_set = + Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Alonzo); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Alonzo(AlonzoProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 16384, + languages: vec![Language::PlutusV1, Language::PlutusV2], + max_block_ex_mem: 50000000, + max_block_ex_steps: 40000000000, + max_tx_ex_mem: 10000000, + max_tx_ex_steps: 10000000000, + max_val_size: 5000, + collateral_percent: 150, + max_collateral_inputs: 3, + coints_per_utxo_word: 34482, + }), + prot_magic: 764824073, + block_slot: 44237276, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Missing verification key witness"), + Err(err) => match err { + Alonzo(MissingVKWitness) => (), + _ => assert!(false, "Unexpected error ({:?})", err), + }, + } + } + + #[test] + // Same as successful_mainnet_tx, except that the signature of the only witness + // of the transaction is modified. + fn wrong_signature() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/alonzo1.tx")); + let mut mtx: MintedTx = minted_tx_from_cbor(&cbor_bytes); + let utxos: UTxOs = mk_utxo_for_alonzo_compatible_tx( + &mtx.transaction_body, + &[( + String::from(include_str!("../../test_data/alonzo1.address")), + Value::Coin(1549646822), + None, + )], + ); + let mut tx_wits: MintedWitnessSet = mtx.transaction_witness_set.unwrap().clone(); + let mut wit: VKeyWitness = tx_wits.vkeywitness.clone().unwrap().pop().unwrap(); + // "c50047bafa1adfbfd588d7c8be89f7ab17aecd47c4cc0ed5c1318caca57c8215d77d6878f0eb2bd2620b4ea552415a3028f98102275c9a564278d0f4e6425d02" + // is replaced with + // "c50047bafa1adfbfd588d7c8be89f7ab17aecd47c4cc0ed5c1318caca57c8215d77d6878f0eb2bd2620b4ea552415a3028f98102275c9a564278d0f400000000" + wit.signature = hex::decode( + "c50047bafa1adfbfd588d7c8be89f7ab17aecd47c4cc0ed5c1318caca57c8215d77d6878f0eb2bd2620b4ea552415a3028f98102275c9a564278d0f400000000" + ).unwrap().into(); + tx_wits.vkeywitness = Some(vec![wit]); + let mut tx_buf: Vec = Vec::new(); + match encode(tx_wits, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?})", err), + }; + mtx.transaction_witness_set = + Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let metx: MultiEraTx = MultiEraTx::from_alonzo_compatible(&mtx, Era::Alonzo); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Alonzo(AlonzoProtParams { + fee_policy: FeePolicy { + summand: 155381, + multiplier: 44, + }, + max_tx_size: 16384, + languages: vec![Language::PlutusV1, Language::PlutusV2], + max_block_ex_mem: 50000000, + max_block_ex_steps: 40000000000, + max_tx_ex_mem: 10000000, + max_tx_ex_steps: 10000000000, + max_val_size: 5000, + collateral_percent: 150, + max_collateral_inputs: 3, + coints_per_utxo_word: 34482, + }), + prot_magic: 764824073, + block_slot: 44237276, + network_id: 1, + }; + match validate(&metx, &utxos, &env) { + Ok(()) => assert!(false, "Witness signature should verify the transaction"), + Err(err) => match err { + Alonzo(VKWrongSignature) => (), + _ => assert!(false, "Unexpected error ({:?})", err), + }, + } + } }