From 445ffc483d4d0c001fb47dcc44ecb3c950b51d41 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Thu, 8 Aug 2024 16:20:04 +0200 Subject: [PATCH] Further remove todos for v3, and reduce duplication in transaction evaluation --- crates/uplc/src/tx.rs | 15 +- crates/uplc/src/tx/error.rs | 12 +- crates/uplc/src/tx/eval.rs | 690 +++++---------------------- crates/uplc/src/tx/phase_one.rs | 3 +- crates/uplc/src/tx/script_context.rs | 373 +++++++++++++-- crates/uplc/src/tx/to_plutus_data.rs | 8 +- 6 files changed, 468 insertions(+), 633 deletions(-) diff --git a/crates/uplc/src/tx.rs b/crates/uplc/src/tx.rs index 98d15ef6..cb8a4b7a 100644 --- a/crates/uplc/src/tx.rs +++ b/crates/uplc/src/tx.rs @@ -4,14 +4,13 @@ use crate::{ PlutusData, }; use error::Error; -use eval::get_script_and_datum_lookup_table; use pallas_primitives::{ conway::{CostMdls, MintedTx, Redeemer, TransactionInput, TransactionOutput}, Fragment, }; use pallas_traverse::{Era, MultiEraTx}; pub use phase_one::eval_phase_one; -pub use script_context::{ResolvedInput, SlotConfig}; +pub use script_context::{DataLookupTable, ResolvedInput, SlotConfig}; pub mod error; pub mod eval; @@ -37,7 +36,7 @@ pub fn eval_phase_two( ) -> Result, Error> { let redeemers = tx.transaction_witness_set.redeemer.as_ref(); - let lookup_table = get_script_and_datum_lookup_table(tx, utxos); + let lookup_table = DataLookupTable::from_transaction(tx, utxos); if run_phase_one { // subset of phase 1 check on redeemers and scripts @@ -123,9 +122,6 @@ pub fn eval_phase_two_raw( }; match multi_era_tx { - MultiEraTx::Babbage(_) => { - todo!("convert Babbage's tx into Conway's") - } MultiEraTx::Conway(tx) => { match eval_phase_two( &tx, @@ -143,7 +139,12 @@ pub fn eval_phase_two_raw( Err(err) => Err(err), } } - _ => todo!("Wrong era. Please use a more recent transaction format"), + _ => unimplemented!( + r#"The transaction is serialized in an old era format. Because we're slightly lazy to +maintain backward compatibility with every possible transaction format AND, because +those formats are mostly forward-compatible, you are kindly expected to provide a +transaction in a format suitable for the Conway era."# + ), } } diff --git a/crates/uplc/src/tx/error.rs b/crates/uplc/src/tx/error.rs index 745e91cf..97dd4089 100644 --- a/crates/uplc/src/tx/error.rs +++ b/crates/uplc/src/tx/error.rs @@ -29,8 +29,10 @@ pub enum Error { ExtraneousRedeemer, #[error("Resolved Input not found.")] ResolvedInputNotFound(TransactionInput), - #[error("A key hash cannot be the hash of a script.")] - ScriptKeyHash, + #[error("Redeemer points to a non-script withdrawal.")] + NonScriptWithdrawal, + #[error("Stake credential points to a non-script withdrawal.")] + NonScriptStakeCredential, #[error("Cost model not found for language: {:?}.", .0)] CostModelNotFound(Language), #[error("Wrong era, Please use Babbage or Alonzo: {0}")] @@ -49,14 +51,16 @@ pub enum Error { MissingRequiredScript { hash: String }, #[error("Missing required inline datum or datum hash in script input.")] MissingRequiredInlineDatumOrHash, - #[error("Only stake deregistration and delegation are valid certificate script purposes.")] - OnlyStakeDeregAndDelegAllowed, + #[error("Redeemer points to an unsupported certificate type.")] + UnsupportedCertificateType, #[error("Redeemer ({}, {}): {}", tag, index, err)] RedeemerError { tag: String, index: u32, err: Box, }, + #[error("Missing script for redeemer")] + MissingScriptForRedeemer, #[error("Failed to apply parameters to Plutus script.")] ApplyParamsError, } diff --git a/crates/uplc/src/tx/eval.rs b/crates/uplc/src/tx/eval.rs index fee67505..14cf666d 100644 --- a/crates/uplc/src/tx/eval.rs +++ b/crates/uplc/src/tx/eval.rs @@ -1,420 +1,18 @@ use super::{ - script_context::{ResolvedInput, ScriptContext, ScriptPurpose, SlotConfig, TxInfo}, + script_context::{find_script, ResolvedInput, ScriptContext, SlotConfig, TxInfo}, to_plutus_data::ToPlutusData, Error, }; use crate::{ ast::{FakeNamedDeBruijn, NamedDeBruijn, Program}, machine::cost_model::ExBudget, - tx::script_context::{TxInfoV1, TxInfoV2, TxInfoV3}, + tx::script_context::{DataLookupTable, ScriptVersion, TxInfoV1, TxInfoV2}, PlutusData, }; -use pallas_addresses::{Address, ScriptHash, StakePayload}; -use pallas_codec::utils::{Bytes, NonEmptyKeyValuePairs, NonEmptySet}; -use pallas_crypto::hash::Hash; +use pallas_codec::utils::Bytes; use pallas_primitives::conway::{ - Certificate, CostMdls, CostModel, DatumHash, DatumOption, ExUnits, Language, Mint, MintedTx, - NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script, PolicyId, PseudoScript, Redeemer, - RedeemerTag, RewardAccount, StakeCredential, TransactionInput, TransactionOutput, Withdrawals, + CostMdls, CostModel, ExUnits, Language, MintedTx, Redeemer, RedeemerTag, }; -use pallas_traverse::{ComputeHash, OriginalHash}; -use std::{collections::HashMap, convert::TryInto, vec}; - -fn redeemer_tag_to_string(redeemer_tag: &RedeemerTag) -> String { - match redeemer_tag { - RedeemerTag::Spend => "Spend".to_string(), - RedeemerTag::Mint => "Mint".to_string(), - RedeemerTag::Cert => "Cert".to_string(), - RedeemerTag::Reward => "Reward".to_string(), - tag => todo!("redeemer_tag_to_string for {tag:?}"), - } -} - -#[derive(Debug, PartialEq, Clone)] -pub enum ScriptVersion { - Native(NativeScript), - V1(PlutusV1Script), - V2(PlutusV2Script), - V3(PlutusV3Script), -} - -#[derive(Debug, PartialEq, Clone)] -enum ExecutionPurpose { - WithDatum(ScriptVersion, PlutusData), // Spending - NoDatum(ScriptVersion), // Minting, Wdrl, DCert -} - -pub struct DataLookupTable { - datum: HashMap, - scripts: HashMap, -} - -impl DataLookupTable { - pub fn scripts(&self) -> HashMap { - self.scripts.clone() - } -} - -fn get_script_purpose( - redeemer: &Redeemer, - inputs: &[TransactionInput], - mint: &Option, - dcert: &Option>, - wdrl: &Option, -) -> Result { - // sorting according to specs section 4.1: https://hydra.iohk.io/build/18583827/download/1/alonzo-changes.pdf - let tag = redeemer.tag; - let index = redeemer.index; - match tag { - RedeemerTag::Mint => { - // sort lexical by policy id - let mut policy_ids = mint - .as_ref() - .unwrap_or(&NonEmptyKeyValuePairs::Indef(vec![])) - .iter() - .map(|(policy_id, _)| *policy_id) - .collect::>(); - policy_ids.sort(); - match policy_ids.get(index as usize) { - Some(policy_id) => Ok(ScriptPurpose::Minting(*policy_id)), - None => Err(Error::ExtraneousRedeemer), - } - } - RedeemerTag::Spend => { - // sort lexical by tx_hash and index - let mut inputs = inputs.to_vec(); - inputs.sort(); - match inputs.get(index as usize) { - Some(input) => Ok(ScriptPurpose::Spending(input.clone())), - None => Err(Error::ExtraneousRedeemer), - } - } - RedeemerTag::Reward => { - // sort lexical by reward account - let mut reward_accounts = wdrl - .as_ref() - .unwrap_or(&NonEmptyKeyValuePairs::Indef(vec![])) - .iter() - .map(|(racnt, _)| racnt.clone()) - .collect::>(); - reward_accounts.sort(); - let reward_account = match reward_accounts.get(index as usize) { - Some(ra) => ra.clone(), - None => return Err(Error::ExtraneousRedeemer), - }; - let address = Address::from_bytes(&reward_account)?; - let credential = match address { - Address::Stake(stake_address) => match stake_address.payload() { - StakePayload::Script(script_hash) => StakeCredential::Scripthash(*script_hash), - StakePayload::Stake(_) => { - return Err(Error::ScriptKeyHash); - } - }, - _ => return Err(Error::BadWithdrawalAddress), - }; - Ok(ScriptPurpose::Rewarding(credential)) - } - RedeemerTag::Cert => { - // sort by order given in the tx (just take it as it is basically) - match dcert.as_deref().unwrap_or(&vec![]).get(index as usize) { - Some(cert) => Ok(ScriptPurpose::Certifying(cert.clone())), - None => Err(Error::ExtraneousRedeemer), - } - } - tag => todo!("get_script_purpose for {tag:?}"), - } -} - -fn get_execution_purpose( - utxos: &[ResolvedInput], - script_purpose: &ScriptPurpose, - lookup_table: &DataLookupTable, -) -> Result { - match script_purpose { - ScriptPurpose::Minting(policy_id) => { - let policy_id_array: [u8; 28] = policy_id.to_vec().try_into().unwrap(); - let hash = Hash::from(policy_id_array); - - let script = match lookup_table.scripts.get(&hash) { - Some(s) => s.clone(), - None => { - return Err(Error::MissingRequiredScript { - hash: hash.to_string(), - }); - } - }; - Ok(ExecutionPurpose::NoDatum(script)) - } - ScriptPurpose::Spending(out_ref) => { - let utxo = match utxos.iter().find(|utxo| utxo.input == *out_ref) { - Some(resolved) => resolved, - None => return Err(Error::ResolvedInputNotFound(out_ref.clone())), - }; - match &utxo.output { - TransactionOutput::Legacy(output) => { - let address = Address::from_bytes(&output.address).unwrap(); - match address { - Address::Shelley(shelley_address) => { - let hash = shelley_address.payment().as_hash(); - let script = match lookup_table.scripts.get(hash) { - Some(s) => s.clone(), - None => { - return Err(Error::MissingRequiredScript { - hash: hash.to_string(), - }); - } - }; - - let datum_hash = match &output.datum_hash { - Some(hash) => hash, - None => return Err(Error::MissingRequiredInlineDatumOrHash), - }; - - let datum = match lookup_table.datum.get(datum_hash) { - Some(d) => d.clone(), - None => { - return Err(Error::MissingRequiredDatum { - hash: datum_hash.to_string(), - }); - } - }; - - Ok(ExecutionPurpose::WithDatum(script, datum)) - } - _ => Err(Error::ScriptKeyHash), - } - } - TransactionOutput::PostAlonzo(output) => { - let address = Address::from_bytes(&output.address).unwrap(); - match address { - Address::Shelley(shelley_address) => { - let hash = shelley_address.payment().as_hash(); - let script = match lookup_table.scripts.get(hash) { - Some(s) => s.clone(), - None => { - return Err(Error::MissingRequiredScript { - hash: hash.to_string(), - }); - } - }; - - let datum = match &output.datum_option { - Some(DatumOption::Hash(hash)) => { - match lookup_table.datum.get(hash) { - Some(d) => d.clone(), - None => { - return Err(Error::MissingRequiredDatum { - hash: hash.to_string(), - }); - } - } - } - Some(DatumOption::Data(data)) => data.0.clone(), - _ => return Err(Error::MissingRequiredInlineDatumOrHash), - }; - - Ok(ExecutionPurpose::WithDatum(script, datum)) - } - _ => Err(Error::ScriptKeyHash), - } - } - } - } - ScriptPurpose::Rewarding(stake_credential) => { - let script_hash = match stake_credential { - StakeCredential::Scripthash(hash) => *hash, - _ => return Err(Error::ScriptKeyHash), - }; - - let script = match lookup_table.scripts.get(&script_hash) { - Some(s) => s.clone(), - None => { - return Err(Error::MissingRequiredScript { - hash: script_hash.to_string(), - }); - } - }; - - Ok(ExecutionPurpose::NoDatum(script)) - } - ScriptPurpose::Certifying(cert) => match cert { - Certificate::StakeDeregistration(stake_credential) => { - let script_hash = match stake_credential { - StakeCredential::Scripthash(hash) => *hash, - _ => return Err(Error::ScriptKeyHash), - }; - - let script = match lookup_table.scripts.get(&script_hash) { - Some(s) => s.clone(), - None => { - return Err(Error::MissingRequiredScript { - hash: script_hash.to_string(), - }); - } - }; - - Ok(ExecutionPurpose::NoDatum(script)) - } - Certificate::StakeDelegation(stake_credential, _) => { - let script_hash = match stake_credential { - StakeCredential::Scripthash(hash) => *hash, - _ => return Err(Error::ScriptKeyHash), - }; - - let script = match lookup_table.scripts.get(&script_hash) { - Some(s) => s.clone(), - None => { - return Err(Error::MissingRequiredScript { - hash: script_hash.to_string(), - }); - } - }; - - Ok(ExecutionPurpose::NoDatum(script)) - } - _ => Err(Error::OnlyStakeDeregAndDelegAllowed), - }, - } -} - -pub fn get_script_and_datum_lookup_table( - tx: &MintedTx, - utxos: &[ResolvedInput], -) -> DataLookupTable { - let mut datum = HashMap::new(); - let mut scripts = HashMap::new(); - - // discovery in witness set - - let plutus_data_witnesses = tx - .transaction_witness_set - .plutus_data - .clone() - .map(|s| s.to_vec()) - .unwrap_or_default(); - - let scripts_native_witnesses = tx - .transaction_witness_set - .native_script - .clone() - .map(|s| s.to_vec()) - .unwrap_or_default(); - - let scripts_v1_witnesses = tx - .transaction_witness_set - .plutus_v1_script - .clone() - .map(|s| s.to_vec()) - .unwrap_or_default(); - - let scripts_v2_witnesses = tx - .transaction_witness_set - .plutus_v2_script - .clone() - .map(|s| s.to_vec()) - .unwrap_or_default(); - - let scripts_v3_witnesses = tx - .transaction_witness_set - .plutus_v3_script - .clone() - .map(|s| s.to_vec()) - .unwrap_or_default(); - - for plutus_data in plutus_data_witnesses.iter() { - datum.insert(plutus_data.original_hash(), plutus_data.clone().unwrap()); - } - - for script in scripts_native_witnesses.iter() { - scripts.insert( - script.compute_hash(), - ScriptVersion::Native(script.clone().unwrap()), - ); - } - - for script in scripts_v1_witnesses.iter() { - scripts.insert(script.compute_hash(), ScriptVersion::V1(script.clone())); - } - - for script in scripts_v2_witnesses.iter() { - scripts.insert(script.compute_hash(), ScriptVersion::V2(script.clone())); - } - - for script in scripts_v3_witnesses.iter() { - scripts.insert(script.compute_hash(), ScriptVersion::V3(script.clone())); - } - - // discovery in utxos (script ref) - - for utxo in utxos.iter() { - match &utxo.output { - TransactionOutput::Legacy(_) => {} - TransactionOutput::PostAlonzo(output) => { - if let Some(script) = &output.script_ref { - match &script.0 { - PseudoScript::NativeScript(ns) => { - scripts.insert(ns.compute_hash(), ScriptVersion::Native(ns.clone())); - } - PseudoScript::PlutusV1Script(v1) => { - scripts.insert(v1.compute_hash(), ScriptVersion::V1(v1.clone())); - } - PseudoScript::PlutusV2Script(v2) => { - scripts.insert(v2.compute_hash(), ScriptVersion::V2(v2.clone())); - } - PseudoScript::PlutusV3Script(v3) => { - scripts.insert(v3.compute_hash(), ScriptVersion::V3(v3.clone())); - } - } - } - } - } - } - - DataLookupTable { datum, scripts } -} - -fn mk_redeemer_with_datum( - cost_mdl_opt: Option<&CostModel>, - initial_budget: &ExBudget, - lang: &Language, - datum: PlutusData, - (redeemer, purpose): (&Redeemer, ScriptPurpose), - tx_info: TxInfo, - program: Program, -) -> Result { - let script_context = ScriptContext { tx_info, purpose }; - - let program = program - .apply_data(datum) - .apply_data(redeemer.data.clone()) - .apply_data(script_context.to_plutus_data()); - - let mut eval_result = if let Some(costs) = cost_mdl_opt { - program.eval_as(lang, costs, Some(initial_budget)) - } else { - program.eval_version(ExBudget::default(), lang) - }; - - let cost = eval_result.cost(); - let logs = eval_result.logs(); - - match eval_result.result() { - Ok(_) => (), - Err(err) => return Err(Error::Machine(err, cost, logs)), - } - - let new_redeemer = Redeemer { - tag: redeemer.tag, - index: redeemer.index, - data: redeemer.data.clone(), - ex_units: ExUnits { - mem: cost.mem as u64, - steps: cost.cpu as u64, - }, - }; - - Ok(new_redeemer) -} pub fn eval_redeemer( tx: &MintedTx, @@ -425,188 +23,116 @@ pub fn eval_redeemer( cost_mdls_opt: Option<&CostMdls>, initial_budget: &ExBudget, ) -> Result { - let result = || { - let purpose = get_script_purpose( - redeemer, - &tx.transaction_body.inputs, - &tx.transaction_body.mint, - &tx.transaction_body.certificates, - &tx.transaction_body.withdrawals, - )?; + fn do_eval_redeemer( + cost_mdl_opt: Option<&CostModel>, + initial_budget: &ExBudget, + lang: &Language, + datum: Option, + redeemer: &Redeemer, + tx_info: TxInfo, + program: Program, + ) -> Result { + let purpose = tx_info + .purpose(redeemer) + .expect("redeemer's purpose shall be known by this point."); - let program = |script: Bytes| { - let mut buffer = Vec::new(); - Program::::from_cbor(&script, &mut buffer) - .map(Into::>::into) + let script_context = ScriptContext { tx_info, purpose }; + + let program = if let Some(datum) = datum { + program.apply_data(datum) + } else { + program + } + .apply_data(redeemer.data.clone()) + .apply_data(script_context.to_plutus_data()); + + let mut eval_result = if let Some(costs) = cost_mdl_opt { + program.eval_as(lang, costs, Some(initial_budget)) + } else { + program.eval_version(ExBudget::default(), lang) }; - let execution_purpose: ExecutionPurpose = - get_execution_purpose(utxos, &purpose, lookup_table)?; + let cost = eval_result.cost(); + let logs = eval_result.logs(); - match execution_purpose { - ExecutionPurpose::WithDatum(script_version, datum) => match script_version { - ScriptVersion::V1(script) => mk_redeemer_with_datum( - cost_mdls_opt - .map(|cost_mdls| { - cost_mdls - .plutus_v1 - .as_ref() - .ok_or(Error::CostModelNotFound(Language::PlutusV1)) - }) - .transpose()?, - initial_budget, - &Language::PlutusV1, - datum, - (redeemer, purpose), - TxInfoV1::from_transaction(tx, utxos, slot_config)?, - program(script.0)?, - ), - - ScriptVersion::V2(script) => mk_redeemer_with_datum( - cost_mdls_opt - .map(|cost_mdls| { - cost_mdls - .plutus_v2 - .as_ref() - .ok_or(Error::CostModelNotFound(Language::PlutusV2)) - }) - .transpose()?, - initial_budget, - &Language::PlutusV2, - datum, - (redeemer, purpose), - TxInfoV2::from_transaction(tx, utxos, slot_config)?, - program(script.0)?, - ), - - ScriptVersion::V3(script) => mk_redeemer_with_datum( - cost_mdls_opt - .map(|cost_mdls| { - cost_mdls - .plutus_v3 - .as_ref() - .ok_or(Error::CostModelNotFound(Language::PlutusV3)) - }) - .transpose()?, - initial_budget, - &Language::PlutusV2, - datum, - (redeemer, purpose), - TxInfoV3::from_transaction(tx, utxos, slot_config)?, - program(script.0)?, - ), - - ScriptVersion::Native(_) => Err(Error::NativeScriptPhaseTwo), - }, - ExecutionPurpose::NoDatum(script_version) => match script_version { - ScriptVersion::V1(script) => { - let tx_info = TxInfoV1::from_transaction(tx, utxos, slot_config)?; - let script_context = ScriptContext { tx_info, purpose }; - - let program: Program = { - let mut buffer = Vec::new(); - - let prog = Program::::from_cbor(&script.0, &mut buffer)?; - - prog.into() - }; - - let program = program - .apply_data(redeemer.data.clone()) - .apply_data(script_context.to_plutus_data()); - - let mut eval_result = if let Some(cost_mdls) = cost_mdls_opt { - let costs = if let Some(costs) = &cost_mdls.plutus_v1 { - costs - } else { - return Err(Error::CostModelNotFound(Language::PlutusV1)); - }; - - program.eval_as(&Language::PlutusV1, costs, Some(initial_budget)) - } else { - program.eval_version(ExBudget::default(), &Language::PlutusV1) - }; - - let cost = eval_result.cost(); - let logs = eval_result.logs(); - - match eval_result.result() { - Ok(_) => (), - Err(err) => return Err(Error::Machine(err, cost, logs)), - } - - let new_redeemer = Redeemer { - tag: redeemer.tag, - index: redeemer.index, - data: redeemer.data.clone(), - ex_units: ExUnits { - mem: cost.mem as u64, - steps: cost.cpu as u64, - }, - }; - - Ok(new_redeemer) - } - ScriptVersion::V2(script) => { - let tx_info = TxInfoV2::from_transaction(tx, utxos, slot_config)?; - let script_context = ScriptContext { tx_info, purpose }; - - let program: Program = { - let mut buffer = Vec::new(); - - let prog = Program::::from_cbor(&script.0, &mut buffer)?; - - prog.into() - }; - - let program = program - .apply_data(redeemer.data.clone()) - .apply_data(script_context.to_plutus_data()); - - let mut eval_result = if let Some(cost_mdls) = cost_mdls_opt { - let costs = if let Some(costs) = &cost_mdls.plutus_v2 { - costs - } else { - return Err(Error::CostModelNotFound(Language::PlutusV2)); - }; - - program.eval_as(&Language::PlutusV2, costs, Some(initial_budget)) - } else { - program.eval(ExBudget::default()) - }; - - let cost = eval_result.cost(); - let logs = eval_result.logs(); - - match eval_result.result() { - Ok(_) => (), - Err(err) => return Err(Error::Machine(err, cost, logs)), - } - - let new_redeemer = Redeemer { - tag: redeemer.tag, - index: redeemer.index, - data: redeemer.data.clone(), - ex_units: ExUnits { - mem: cost.mem as u64, - steps: cost.cpu as u64, - }, - }; - - Ok(new_redeemer) - } - ScriptVersion::V3(_script) => todo!(), - ScriptVersion::Native(_) => Err(Error::NativeScriptPhaseTwo), - }, + match eval_result.result() { + Ok(_) => (), + Err(err) => return Err(Error::Machine(err, cost, logs)), } + + let new_redeemer = Redeemer { + tag: redeemer.tag, + index: redeemer.index, + data: redeemer.data.clone(), + ex_units: ExUnits { + mem: cost.mem as u64, + steps: cost.cpu as u64, + }, + }; + + Ok(new_redeemer) + } + + let program = |script: Bytes| { + let mut buffer = Vec::new(); + Program::::from_cbor(&script, &mut buffer) + .map(Into::>::into) }; - match result() { - Ok(r) => Ok(r), - Err(err) => Err(Error::RedeemerError { - tag: redeemer_tag_to_string(&redeemer.tag), - index: redeemer.index, - err: Box::new(err), - }), + match find_script(redeemer, tx, utxos, lookup_table)? { + (ScriptVersion::Native(_), _) => Err(Error::NativeScriptPhaseTwo), + + (ScriptVersion::V1(script), datum) => do_eval_redeemer( + cost_mdls_opt + .map(|cost_mdls| { + cost_mdls + .plutus_v1 + .as_ref() + .ok_or(Error::CostModelNotFound(Language::PlutusV2)) + }) + .transpose()?, + initial_budget, + &Language::PlutusV1, + datum, + redeemer, + TxInfoV1::from_transaction(tx, utxos, slot_config)?, + program(script.0)?, + ), + + (ScriptVersion::V2(script), datum) => do_eval_redeemer( + cost_mdls_opt + .map(|cost_mdls| { + cost_mdls + .plutus_v2 + .as_ref() + .ok_or(Error::CostModelNotFound(Language::PlutusV2)) + }) + .transpose()?, + initial_budget, + &Language::PlutusV2, + datum, + redeemer, + TxInfoV2::from_transaction(tx, utxos, slot_config)?, + program(script.0)?, + ), + + (ScriptVersion::V3(_script), _datum) => todo!(), } + .map_err(|err| Error::RedeemerError { + tag: redeemer_tag_to_string(&redeemer.tag), + index: redeemer.index, + err: Box::new(err), + }) +} + +fn redeemer_tag_to_string(redeemer_tag: &RedeemerTag) -> String { + match redeemer_tag { + RedeemerTag::Spend => "Spend", + RedeemerTag::Mint => "Mint", + RedeemerTag::Reward => "Withdraw", + RedeemerTag::Cert => "Publish", + RedeemerTag::Propose => "Propose", + RedeemerTag::Vote => "Vote", + } + .to_string() } diff --git a/crates/uplc/src/tx/phase_one.rs b/crates/uplc/src/tx/phase_one.rs index 6cc8121f..245c039f 100644 --- a/crates/uplc/src/tx/phase_one.rs +++ b/crates/uplc/src/tx/phase_one.rs @@ -1,7 +1,6 @@ use super::{ error::Error, - eval::{DataLookupTable, ScriptVersion}, - script_context::{ResolvedInput, ScriptPurpose}, + script_context::{DataLookupTable, ResolvedInput, ScriptPurpose, ScriptVersion}, }; use itertools::Itertools; use pallas_addresses::{Address, ScriptHash, ShelleyPaymentPart, StakePayload}; diff --git a/crates/uplc/src/tx/script_context.rs b/crates/uplc/src/tx/script_context.rs index 95108729..225b9162 100644 --- a/crates/uplc/src/tx/script_context.rs +++ b/crates/uplc/src/tx/script_context.rs @@ -7,14 +7,14 @@ use pallas_primitives::{ alonzo, conway::{ AddrKeyhash, Certificate, Coin, DatumHash, DatumOption, Mint, MintedTransactionBody, - MintedTransactionOutput, MintedTx, MintedWitnessSet, PlutusData, PolicyId, - PostAlonzoTransactionOutput, PseudoDatumOption, Redeemer, RedeemerTag, RedeemersKey, - RequiredSigners, RewardAccount, StakeCredential, TransactionInput, TransactionOutput, - Value, + MintedTransactionOutput, MintedTx, MintedWitnessSet, NativeScript, PlutusData, + PlutusV1Script, PlutusV2Script, PlutusV3Script, PolicyId, PostAlonzoTransactionOutput, + PseudoDatumOption, PseudoScript, Redeemer, RedeemerTag, RedeemersKey, RequiredSigners, + RewardAccount, ScriptHash, StakeCredential, TransactionInput, TransactionOutput, Value, }, }; -use pallas_traverse::OriginalHash; -use std::{cmp::Ordering, ops::Deref}; +use pallas_traverse::{ComputeHash, OriginalHash}; +use std::{cmp::Ordering, collections::HashMap, ops::Deref}; #[derive(Debug, PartialEq, Clone)] pub struct ResolvedInput { @@ -33,6 +33,30 @@ pub enum TxOut { V2(TransactionOutput), } +impl TxOut { + pub fn address(&self) -> Address { + let address_from_output = |output: &TransactionOutput| match output { + TransactionOutput::Legacy(x) => Address::from_bytes(&x.address).unwrap(), + TransactionOutput::PostAlonzo(x) => Address::from_bytes(&x.address).unwrap(), + }; + match self { + TxOut::V1(output) => address_from_output(output), + TxOut::V2(output) => address_from_output(output), + } + } + + pub fn datum(&self) -> Option { + let datum_from_output = |output: &TransactionOutput| match output { + TransactionOutput::Legacy(x) => x.datum_hash.map(DatumOption::Hash), + TransactionOutput::PostAlonzo(x) => x.datum_option.clone(), + }; + match self { + TxOut::V1(output) => datum_from_output(output), + TxOut::V2(output) => datum_from_output(output), + } + } +} + #[derive(Debug, PartialEq, Eq, Clone)] pub enum ScriptPurpose { Minting(PolicyId), @@ -41,17 +65,133 @@ pub enum ScriptPurpose { Certifying(Certificate), } +#[derive(Debug, PartialEq, Clone)] +pub enum ScriptVersion { + Native(NativeScript), + V1(PlutusV1Script), + V2(PlutusV2Script), + V3(PlutusV3Script), +} + +pub struct DataLookupTable { + datum: HashMap, + scripts: HashMap, +} + +impl DataLookupTable { + pub fn from_transaction(tx: &MintedTx, utxos: &[ResolvedInput]) -> DataLookupTable { + let mut datum = HashMap::new(); + let mut scripts = HashMap::new(); + + // discovery in witness set + + let plutus_data_witnesses = tx + .transaction_witness_set + .plutus_data + .clone() + .map(|s| s.to_vec()) + .unwrap_or_default(); + + let scripts_native_witnesses = tx + .transaction_witness_set + .native_script + .clone() + .map(|s| s.to_vec()) + .unwrap_or_default(); + + let scripts_v1_witnesses = tx + .transaction_witness_set + .plutus_v1_script + .clone() + .map(|s| s.to_vec()) + .unwrap_or_default(); + + let scripts_v2_witnesses = tx + .transaction_witness_set + .plutus_v2_script + .clone() + .map(|s| s.to_vec()) + .unwrap_or_default(); + + let scripts_v3_witnesses = tx + .transaction_witness_set + .plutus_v3_script + .clone() + .map(|s| s.to_vec()) + .unwrap_or_default(); + + for plutus_data in plutus_data_witnesses.iter() { + datum.insert(plutus_data.original_hash(), plutus_data.clone().unwrap()); + } + + for script in scripts_native_witnesses.iter() { + scripts.insert( + script.compute_hash(), + ScriptVersion::Native(script.clone().unwrap()), + ); + } + + for script in scripts_v1_witnesses.iter() { + scripts.insert(script.compute_hash(), ScriptVersion::V1(script.clone())); + } + + for script in scripts_v2_witnesses.iter() { + scripts.insert(script.compute_hash(), ScriptVersion::V2(script.clone())); + } + + for script in scripts_v3_witnesses.iter() { + scripts.insert(script.compute_hash(), ScriptVersion::V3(script.clone())); + } + + // discovery in utxos (script ref) + + for utxo in utxos.iter() { + match &utxo.output { + TransactionOutput::Legacy(_) => {} + TransactionOutput::PostAlonzo(output) => { + if let Some(script) = &output.script_ref { + match &script.0 { + PseudoScript::NativeScript(ns) => { + scripts + .insert(ns.compute_hash(), ScriptVersion::Native(ns.clone())); + } + PseudoScript::PlutusV1Script(v1) => { + scripts.insert(v1.compute_hash(), ScriptVersion::V1(v1.clone())); + } + PseudoScript::PlutusV2Script(v2) => { + scripts.insert(v2.compute_hash(), ScriptVersion::V2(v2.clone())); + } + PseudoScript::PlutusV3Script(v3) => { + scripts.insert(v3.compute_hash(), ScriptVersion::V3(v3.clone())); + } + } + } + } + } + } + + DataLookupTable { datum, scripts } + } +} + +impl DataLookupTable { + pub fn scripts(&self) -> HashMap { + self.scripts.clone() + } +} + #[derive(Debug, PartialEq, Clone)] pub struct TxInfoV1 { pub inputs: Vec, pub outputs: Vec, pub fee: Value, pub mint: MintValue, - pub dcert: Vec, - pub wdrl: Vec<(Address, Coin)>, + pub certificates: Vec, + pub withdrawals: Vec<(Address, Coin)>, pub valid_range: TimeRange, pub signatories: Vec, pub data: Vec<(DatumHash, PlutusData)>, + pub redeemers: KeyValuePairs, pub id: Hash<32>, } @@ -65,16 +205,28 @@ impl TxInfoV1 { return Err(Error::ScriptAndInputRefNotAllowed); } + let inputs = get_tx_in_info_v1(&tx.transaction_body.inputs, utxos)?; + let certificates = get_certificates_info(&tx.transaction_body.certificates); + let withdrawals = + KeyValuePairs::from(get_withdrawal_info(&tx.transaction_body.withdrawals)); + let mint = get_mint_info(&tx.transaction_body.mint); + + let redeemers = get_redeemers_info( + &tx.transaction_witness_set, + script_purpose_builder(&inputs[..], &mint, &certificates, &withdrawals), + )?; + Ok(TxInfo::V1(TxInfoV1 { - inputs: get_tx_in_info_v1(&tx.transaction_body.inputs, utxos)?, + inputs, outputs: get_outputs_info(TxOut::V1, &tx.transaction_body.outputs[..]), fee: get_fee_info(&tx.transaction_body.fee), - mint: get_mint_info(&tx.transaction_body.mint), - dcert: get_certificates_info(&tx.transaction_body.certificates), - wdrl: get_withdrawal_info(&tx.transaction_body.withdrawals), + mint, + certificates, + withdrawals: withdrawals.into(), valid_range: get_validity_range_info(&tx.transaction_body, slot_config), signatories: get_signatories_info(&tx.transaction_body.required_signers), data: get_data_info(&tx.transaction_witness_set), + redeemers, id: tx.transaction_body.original_hash(), })) } @@ -87,8 +239,8 @@ pub struct TxInfoV2 { pub outputs: Vec, pub fee: Value, pub mint: MintValue, - pub dcert: Vec, - pub wdrl: KeyValuePairs, + pub certificates: Vec, + pub withdrawals: KeyValuePairs, pub valid_range: TimeRange, pub signatories: Vec, pub redeemers: KeyValuePairs, @@ -103,13 +255,14 @@ impl TxInfoV2 { slot_config: &SlotConfig, ) -> Result { let inputs = get_tx_in_info_v2(&tx.transaction_body.inputs, utxos)?; - let dcert = get_certificates_info(&tx.transaction_body.certificates); - let wdrl = KeyValuePairs::from(get_withdrawal_info(&tx.transaction_body.withdrawals)); + let certificates = get_certificates_info(&tx.transaction_body.certificates); + let withdrawals = + KeyValuePairs::from(get_withdrawal_info(&tx.transaction_body.withdrawals)); let mint = get_mint_info(&tx.transaction_body.mint); let redeemers = get_redeemers_info( &tx.transaction_witness_set, - script_purpose_builder(&inputs[..], &mint, &dcert, &wdrl), + script_purpose_builder(&inputs[..], &mint, &certificates, &withdrawals), )?; let reference_inputs = tx @@ -126,8 +279,8 @@ impl TxInfoV2 { outputs: get_outputs_info(TxOut::V2, &tx.transaction_body.outputs[..]), fee: get_fee_info(&tx.transaction_body.fee), mint, - dcert, - wdrl, + certificates, + withdrawals, valid_range: get_validity_range_info(&tx.transaction_body, slot_config), signatories: get_signatories_info(&tx.transaction_body.required_signers), data: KeyValuePairs::from(get_data_info(&tx.transaction_witness_set)), @@ -155,6 +308,50 @@ pub enum TxInfo { V2(TxInfoV2), } +impl TxInfo { + pub fn purpose(&self, needle: &Redeemer) -> Option { + match self { + TxInfo::V1(TxInfoV1 { redeemers, .. }) | TxInfo::V2(TxInfoV2 { redeemers, .. }) => { + redeemers.iter().find_map(|(purpose, redeemer)| { + if redeemer == needle { + Some(purpose.clone()) + } else { + None + } + }) + } + } + } + + pub fn inputs(&self) -> &[TxInInfo] { + match self { + TxInfo::V1(info) => &info.inputs, + TxInfo::V2(info) => &info.inputs, + } + } + + pub fn mint(&self) -> &MintValue { + match self { + TxInfo::V1(info) => &info.mint, + TxInfo::V2(info) => &info.mint, + } + } + + pub fn withdrawals(&self) -> &[(Address, Coin)] { + match self { + TxInfo::V1(info) => &info.withdrawals[..], + TxInfo::V2(info) => &info.withdrawals[..], + } + } + + pub fn certificates(&self) -> &[Certificate] { + match self { + TxInfo::V1(info) => &info.certificates[..], + TxInfo::V2(info) => &info.certificates[..], + } + } +} + #[derive(Debug, PartialEq, Clone)] pub struct ScriptContext { pub tx_info: TxInfo, @@ -235,7 +432,7 @@ pub fn get_tx_in_info_v1( .collect() } -fn get_tx_in_info_v2( +pub fn get_tx_in_info_v2( inputs: &[TransactionInput], utxos: &[ResolvedInput], ) -> Result, Error> { @@ -271,7 +468,7 @@ fn get_tx_in_info_v2( .collect() } -fn get_mint_info(mint: &Option) -> MintValue { +pub fn get_mint_info(mint: &Option) -> MintValue { MintValue { mint_value: mint .as_ref() @@ -280,7 +477,7 @@ fn get_mint_info(mint: &Option) -> MintValue { } } -fn get_outputs_info( +pub fn get_outputs_info( to_tx_out: fn(TransactionOutput) -> TxOut, outputs: &[MintedTransactionOutput], ) -> Vec { @@ -291,15 +488,15 @@ fn get_outputs_info( .collect() } -fn get_fee_info(fee: &Coin) -> Value { +pub fn get_fee_info(fee: &Coin) -> Value { Value::Coin(*fee) } -fn get_certificates_info(certificates: &Option>) -> Vec { +pub fn get_certificates_info(certificates: &Option>) -> Vec { certificates.clone().map(|s| s.to_vec()).unwrap_or_default() } -fn get_withdrawal_info( +pub fn get_withdrawal_info( withdrawals: &Option>, ) -> Vec<(Address, Coin)> { withdrawals @@ -313,7 +510,10 @@ fn get_withdrawal_info( .unwrap_or_default() } -fn get_validity_range_info(body: &MintedTransactionBody, slot_config: &SlotConfig) -> TimeRange { +pub fn get_validity_range_info( + body: &MintedTransactionBody, + slot_config: &SlotConfig, +) -> TimeRange { fn slot_to_begin_posix_time(slot: u64, sc: &SlotConfig) -> u64 { let ms_after_begin = (slot - sc.zero_slot) * sc.slot_length as u64; sc.zero_time + ms_after_begin @@ -339,14 +539,14 @@ fn get_validity_range_info(body: &MintedTransactionBody, slot_config: &SlotConfi ) } -fn get_signatories_info(signers: &Option) -> Vec { +pub fn get_signatories_info(signers: &Option) -> Vec { signers .as_deref() .map(|s| s.iter().cloned().sorted().collect()) .unwrap_or_default() } -fn get_data_info(witness_set: &MintedWitnessSet) -> Vec<(DatumHash, PlutusData)> { +pub fn get_data_info(witness_set: &MintedWitnessSet) -> Vec<(DatumHash, PlutusData)> { witness_set .plutus_data .as_deref() @@ -360,7 +560,7 @@ fn get_data_info(witness_set: &MintedWitnessSet) -> Vec<(DatumHash, PlutusData)> .unwrap_or_default() } -fn get_redeemers_info<'a>( +pub fn get_redeemers_info<'a>( witness_set: &'a MintedWitnessSet, to_script_purpose: impl Fn(&'a RedeemersKey) -> Result, ) -> Result, Error> { @@ -391,8 +591,8 @@ fn get_redeemers_info<'a>( fn script_purpose_builder<'a>( inputs: &'a [TxInInfo], mint: &'a MintValue, - dcert: &'a [Certificate], - wdrl: &'a KeyValuePairs, + certificates: &'a [Certificate], + withdrawals: &'a KeyValuePairs, ) -> impl Fn(&'a RedeemersKey) -> Result { move |redeemer: &'a RedeemersKey| { let tag = redeemer.tag; @@ -406,8 +606,11 @@ fn script_purpose_builder<'a>( .get(index) .cloned() .map(|i| ScriptPurpose::Spending(i.out_ref)), - RedeemerTag::Cert => dcert.get(index).cloned().map(ScriptPurpose::Certifying), - RedeemerTag::Reward => wdrl + RedeemerTag::Cert => certificates + .get(index) + .cloned() + .map(ScriptPurpose::Certifying), + RedeemerTag::Reward => withdrawals .get(index) .cloned() .map(|(address, _)| match address { @@ -415,7 +618,7 @@ fn script_purpose_builder<'a>( StakePayload::Script(script_hash) => Ok(ScriptPurpose::Rewarding( StakeCredential::Scripthash(*script_hash), )), - StakePayload::Stake(_) => Err(Error::ScriptKeyHash), + StakePayload::Stake(_) => Err(Error::NonScriptWithdrawal), }, _ => Err(Error::BadWithdrawalAddress), }) @@ -426,6 +629,108 @@ fn script_purpose_builder<'a>( } } +pub fn find_script( + redeemer: &Redeemer, + tx: &MintedTx, + utxos: &[ResolvedInput], + lookup_table: &DataLookupTable, +) -> Result<(ScriptVersion, Option), Error> { + let lookup_script = |script_hash: &ScriptHash| match lookup_table.scripts.get(script_hash) { + Some(s) => Ok((s.clone(), None)), + None => Err(Error::MissingRequiredScript { + hash: script_hash.to_string(), + }), + }; + + let lookup_datum = |datum: Option| match datum { + Some(DatumOption::Hash(hash)) => match lookup_table.datum.get(&hash) { + Some(d) => Ok(d.clone()), + None => Err(Error::MissingRequiredDatum { + hash: hash.to_string(), + }), + }, + Some(DatumOption::Data(data)) => Ok(data.0.clone()), + _ => Err(Error::MissingRequiredInlineDatumOrHash), + }; + + match redeemer.tag { + RedeemerTag::Mint => get_mint_info(&tx.transaction_body.mint) + .mint_value + .get(redeemer.index as usize) + .ok_or(Error::MissingScriptForRedeemer) + .and_then(|(policy_id, _)| { + let policy_id_array: [u8; 28] = policy_id.to_vec().try_into().unwrap(); + let hash = Hash::from(policy_id_array); + lookup_script(&hash) + }), + + RedeemerTag::Reward => get_withdrawal_info(&tx.transaction_body.withdrawals) + .get(redeemer.index as usize) + .ok_or(Error::MissingScriptForRedeemer) + .and_then(|(addr, _)| { + let stake_addr = if let Address::Stake(stake_addr) = addr { + stake_addr + } else { + unreachable!("withdrawal always contains stake addresses") + }; + + if let StakePayload::Script(hash) = stake_addr.payload() { + lookup_script(hash) + } else { + Err(Error::NonScriptWithdrawal) + } + }), + + RedeemerTag::Cert => get_certificates_info(&tx.transaction_body.certificates) + .get(redeemer.index as usize) + .ok_or(Error::MissingScriptForRedeemer) + .and_then(|cert| match cert { + Certificate::StakeDeregistration(stake_credential) => match stake_credential { + StakeCredential::Scripthash(hash) => Ok(hash), + _ => Err(Error::NonScriptStakeCredential), + }, + Certificate::StakeDelegation(stake_credential, _) => match stake_credential { + StakeCredential::Scripthash(hash) => Ok(hash), + _ => Err(Error::NonScriptStakeCredential), + }, + Certificate::PoolRetirement { .. } | Certificate::PoolRegistration { .. } => { + Err(Error::UnsupportedCertificateType) + } + _ => { + todo!("remaining certificate types") + } + }) + .and_then(lookup_script), + + RedeemerTag::Spend => get_tx_in_info_v2(&tx.transaction_body.inputs, utxos) + .or_else(|err| { + if matches!(err, Error::ByronAddressNotAllowed) { + get_tx_in_info_v1(&tx.transaction_body.inputs, utxos) + } else { + Err(err) + } + })? + .get(redeemer.index as usize) + .ok_or(Error::MissingScriptForRedeemer) + .and_then(|input| match input.resolved.address() { + Address::Shelley(shelley_address) => { + let hash = shelley_address.payment().as_hash(); + + let script = lookup_script(hash); + + let datum = lookup_datum(input.resolved.datum()); + + script.and_then(|(script, _)| Ok((script, Some(datum?)))) + } + _ => Err(Error::NonScriptStakeCredential), + }), + + RedeemerTag::Propose => todo!("find_script: RedeemerTag::Propose"), + + RedeemerTag::Vote => todo!("find_script: RedeemerTag::Vote"), + } +} + fn from_alonzo_value(value: &alonzo::Value) -> Value { match value { alonzo::Value::Coin(coin) => Value::Coin(*coin), diff --git a/crates/uplc/src/tx/to_plutus_data.rs b/crates/uplc/src/tx/to_plutus_data.rs index 79701b8f..2ed11d45 100644 --- a/crates/uplc/src/tx/to_plutus_data.rs +++ b/crates/uplc/src/tx/to_plutus_data.rs @@ -591,8 +591,8 @@ impl ToPlutusData for TxInfo { tx_info.outputs.to_plutus_data(), tx_info.fee.to_plutus_data(), tx_info.mint.to_plutus_data(), - tx_info.dcert.to_plutus_data(), - tx_info.wdrl.to_plutus_data(), + tx_info.certificates.to_plutus_data(), + tx_info.withdrawals.to_plutus_data(), tx_info.valid_range.to_plutus_data(), tx_info.signatories.to_plutus_data(), tx_info.data.to_plutus_data(), @@ -607,8 +607,8 @@ impl ToPlutusData for TxInfo { tx_info.outputs.to_plutus_data(), tx_info.fee.to_plutus_data(), tx_info.mint.to_plutus_data(), - tx_info.dcert.to_plutus_data(), - tx_info.wdrl.to_plutus_data(), + tx_info.certificates.to_plutus_data(), + tx_info.withdrawals.to_plutus_data(), tx_info.valid_range.to_plutus_data(), tx_info.signatories.to_plutus_data(), tx_info.redeemers.to_plutus_data(),