feat: more errors

This commit is contained in:
rvcas 2022-09-19 00:39:52 -04:00
parent 6e901de2f0
commit 9bab3187b1
No known key found for this signature in database
GPG Key ID: C09B64E263F7D68C
5 changed files with 884 additions and 834 deletions

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,25 @@ use crate::machine;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum Error { pub enum Error {
#[error("Runtime error")] #[error("{0}")]
Address(#[from] pallas_addresses::Error),
#[error("Only shelley reward addresses can be a part of withdrawals")]
BadWithdrawalAddress,
#[error("{0}")]
FragmentDecode(#[from] pallas_primitives::Error),
#[error("{0}")]
Machine(#[from] machine::Error), Machine(#[from] machine::Error),
#[error("Can't eval without redeemers")]
NoRedeemers,
#[error("Mismatch in required redeemers: {} {}", .missing.join(" "), .extra.join(" "))]
RequiredRedeemersMismatch {
missing: Vec<String>,
extra: Vec<String>,
},
#[error("Resolved Input not found")]
ResolvedInputNotFound,
#[error("A key hash cannot be the hash of a script")]
ScriptKeyHash,
#[error("Wrong era, Please use Babbage or Alonzo: {0}")]
WrongEra(#[from] pallas_codec::minicbor::decode::Error),
} }

View File

@ -1,10 +1,16 @@
use std::collections::HashMap; use std::collections::HashMap;
use pallas_addresses::{ScriptHash, Address, ShelleyPaymentPart, StakePayload}; use pallas_addresses::{Address, ScriptHash, ShelleyPaymentPart, StakePayload};
use pallas_codec::utils::{KeyValuePairs, MaybeIndefArray}; use pallas_codec::utils::{KeyValuePairs, MaybeIndefArray};
use pallas_primitives::babbage::{MintedTx, TransactionOutput, StakeCredential, Certificate, RedeemerTag, RewardAccount, PolicyId}; use pallas_primitives::babbage::{
Certificate, MintedTx, PolicyId, RedeemerTag, RewardAccount, StakeCredential, TransactionOutput,
};
use super::{script_context::{ScriptPurpose, ResolvedInput}, eval::{ScriptVersion, DataLookupTable}}; use super::{
error::Error,
eval::{DataLookupTable, ScriptVersion},
script_context::{ResolvedInput, ScriptPurpose},
};
// TODO: include in pallas eventually? // TODO: include in pallas eventually?
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
@ -20,8 +26,8 @@ pub fn eval_phase_one(
tx: &MintedTx, tx: &MintedTx,
utxos: &[ResolvedInput], utxos: &[ResolvedInput],
lookup_table: &DataLookupTable, lookup_table: &DataLookupTable,
) -> anyhow::Result<()> { ) -> Result<(), Error> {
let scripts_needed = scripts_needed(tx, utxos); let scripts_needed = scripts_needed(tx, utxos)?;
validate_missing_scripts(&scripts_needed, lookup_table.scripts())?; validate_missing_scripts(&scripts_needed, lookup_table.scripts())?;
@ -33,41 +39,32 @@ pub fn eval_phase_one(
pub fn validate_missing_scripts( pub fn validate_missing_scripts(
needed: &AlonzoScriptsNeeded, needed: &AlonzoScriptsNeeded,
txscripts: HashMap<ScriptHash, ScriptVersion>, txscripts: HashMap<ScriptHash, ScriptVersion>,
) -> anyhow::Result<()> { ) -> Result<(), Error> {
let received_hashes = txscripts let received_hashes = txscripts.keys().map(|x| *x).collect::<Vec<ScriptHash>>();
.keys()
.map(|x| *x)
.collect::<Vec<ScriptHash>>();
let needed_hashes = needed let needed_hashes = needed.iter().map(|x| x.1).collect::<Vec<ScriptHash>>();
.iter()
.map(|x| x.1)
.collect::<Vec<ScriptHash>>();
let missing: Vec<_> = needed_hashes let missing: Vec<_> = needed_hashes
.clone() .clone()
.into_iter() .into_iter()
.filter(|x| !received_hashes.contains(x)) .filter(|x| !received_hashes.contains(x))
.map(|x| format!( .map(|x| format!("[Missing (sh: {})]", x))
"[Missing (sh: {})]",
x
))
.collect(); .collect();
let extra: Vec<_> = received_hashes let extra: Vec<_> = received_hashes
.into_iter() .into_iter()
.filter(|x| !needed_hashes.contains(x)) .filter(|x| !needed_hashes.contains(x))
.map(|x| format!( .map(|x| format!("[Extraneous (sh: {:?})]", x))
"[Extraneous (sh: {:?})]",
x
))
.collect(); .collect();
if missing.len() > 0 || extra.len() > 0 { if missing.len() > 0 || extra.len() > 0 {
let missing_errors = missing.join(" "); let missing_errors = missing.join(" ");
let extra_errors = extra.join(" "); let extra_errors = extra.join(" ");
unreachable!("Mismatch in required scripts: {} {}", missing_errors, extra_errors); unreachable!(
"Mismatch in required scripts: {} {}",
missing_errors, extra_errors
);
} }
Ok(()) Ok(())
@ -76,36 +73,33 @@ pub fn validate_missing_scripts(
pub fn scripts_needed( pub fn scripts_needed(
tx: &MintedTx, tx: &MintedTx,
utxos: &[ResolvedInput], utxos: &[ResolvedInput],
) -> AlonzoScriptsNeeded { ) -> Result<AlonzoScriptsNeeded, Error> {
let mut needed = Vec::new(); let mut needed = Vec::new();
let txb = tx.transaction_body.clone(); let txb = tx.transaction_body.clone();
let mut spend = txb.inputs let mut spend = Vec::new();
.iter()
.map(|input| { for input in txb.inputs {
let utxo = match utxos.iter().find(|utxo| utxo.input == *input) { let utxo = match utxos.iter().find(|utxo| utxo.input == input) {
Some(u) => u, Some(u) => u,
None => panic!("Resolved input not found."), None => return Err(Error::ResolvedInputNotFound),
}; };
let address = Address::from_bytes(match &utxo.output { let address = Address::from_bytes(match &utxo.output {
TransactionOutput::Legacy(output) => output.address.as_ref(), TransactionOutput::Legacy(output) => output.address.as_ref(),
TransactionOutput::PostAlonzo(output) => output.address.as_ref(), TransactionOutput::PostAlonzo(output) => output.address.as_ref(),
}) })?;
.unwrap();
if let Address::Shelley(a) = address { if let Address::Shelley(a) = address {
if let ShelleyPaymentPart::Script(h) = a.payment() { if let ShelleyPaymentPart::Script(h) = a.payment() {
return Some((ScriptPurpose::Spending(input.clone()), *h)) spend.push((ScriptPurpose::Spending(input.clone()), *h));
}
} }
} }
None let mut reward = txb
}) .withdrawals
.flatten()
.collect::<AlonzoScriptsNeeded>();
let mut reward = txb.withdrawals
.as_ref() .as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![])) .unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter() .iter()
@ -115,7 +109,7 @@ pub fn scripts_needed(
if let Address::Stake(a) = address { if let Address::Stake(a) = address {
if let StakePayload::Script(h) = a.payload() { if let StakePayload::Script(h) = a.payload() {
let cred = StakeCredential::Scripthash(*h); let cred = StakeCredential::Scripthash(*h);
return Some((ScriptPurpose::Rewarding(cred), *h)) return Some((ScriptPurpose::Rewarding(cred), *h));
} }
} }
@ -124,7 +118,8 @@ pub fn scripts_needed(
.flatten() .flatten()
.collect::<AlonzoScriptsNeeded>(); .collect::<AlonzoScriptsNeeded>();
let mut cert = txb.certificates let mut cert = txb
.certificates
.clone() .clone()
.unwrap_or_default() .unwrap_or_default()
.iter() .iter()
@ -133,23 +128,22 @@ pub fn scripts_needed(
match cert { match cert {
Certificate::StakeDeregistration(StakeCredential::Scripthash(h)) => { Certificate::StakeDeregistration(StakeCredential::Scripthash(h)) => {
Some((ScriptPurpose::Certifying(cert.clone()), *h)) Some((ScriptPurpose::Certifying(cert.clone()), *h))
}, }
Certificate::StakeDelegation(StakeCredential::Scripthash(h), _) => { Certificate::StakeDelegation(StakeCredential::Scripthash(h), _) => {
Some((ScriptPurpose::Certifying(cert.clone()), *h)) Some((ScriptPurpose::Certifying(cert.clone()), *h))
}, }
_ => None _ => None,
} }
}) })
.flatten() .flatten()
.collect::<AlonzoScriptsNeeded>(); .collect::<AlonzoScriptsNeeded>();
let mut mint = txb.mint let mut mint = txb
.mint
.as_ref() .as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![])) .unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter() .iter()
.map(|(policy_id, _)| { .map(|(policy_id, _)| (ScriptPurpose::Minting(*policy_id), *policy_id))
(ScriptPurpose::Minting(*policy_id), *policy_id)
})
.collect::<AlonzoScriptsNeeded>(); .collect::<AlonzoScriptsNeeded>();
needed.append(&mut spend); needed.append(&mut spend);
@ -157,91 +151,91 @@ pub fn scripts_needed(
needed.append(&mut cert); needed.append(&mut cert);
needed.append(&mut mint); needed.append(&mut mint);
needed Ok(needed)
} }
/// hasExactSetOfRedeemers in Ledger Spec, but we pass `txscripts` directly /// hasExactSetOfRedeemers in Ledger Spec, but we pass `txscripts` directly
pub fn has_exact_set_of_redeemers( pub fn has_exact_set_of_redeemers(
tx: &MintedTx, tx: &MintedTx,
needed: &AlonzoScriptsNeeded, needed: &AlonzoScriptsNeeded,
txscripts: HashMap<ScriptHash, ScriptVersion>, tx_scripts: HashMap<ScriptHash, ScriptVersion>,
) -> anyhow::Result<()> { ) -> Result<(), Error> {
let redeemers_needed = needed let mut redeemers_needed = Vec::new();
.iter()
.map(|(sp, sh)| {
let rp = rdptr(tx, sp);
let script = txscripts.get(&sh);
match (rp, script) { for (script_purpose, script_hash) in needed {
(Some(ptr), Some(script)) => match script { let redeemer_ptr = build_redeemer_ptr(tx, script_purpose)?;
ScriptVersion::V1(_) => Some((ptr, sp.clone(), *sh)), let script = tx_scripts.get(&script_hash);
ScriptVersion::V2(_) => Some((ptr, sp.clone(), *sh)),
ScriptVersion::Native(_) => None, if let (Some(ptr), Some(script)) = (redeemer_ptr, script) {
}, match script {
_ => None ScriptVersion::V1(_) => {
redeemers_needed.push((ptr, script_purpose.clone(), *script_hash))
}
ScriptVersion::V2(_) => {
redeemers_needed.push((ptr, script_purpose.clone(), *script_hash))
}
ScriptVersion::Native(_) => (),
}
}
} }
})
.flatten()
.collect::<Vec<(RedeemerPtr, ScriptPurpose, ScriptHash)>>();
let wits_rdptrs = tx let wits_redeemer_ptrs: Vec<RedeemerPtr> = tx
.transaction_witness_set .transaction_witness_set
.redeemer .redeemer
.as_ref() .as_ref()
.unwrap_or(&MaybeIndefArray::Indef(vec![])) .unwrap_or(&MaybeIndefArray::Indef(vec![]))
.iter() .iter()
.map(|r| { .map(|r| RedeemerPtr {
RedeemerPtr { tag: r.tag.clone(), index: r.index } tag: r.tag.clone(),
index: r.index,
}) })
.collect::<Vec<RedeemerPtr>>(); .collect();
let needed_rdptrs = redeemers_needed let needed_redeemer_ptrs: Vec<RedeemerPtr> =
.iter() redeemers_needed.iter().map(|x| x.0.clone()).collect();
.map(|x| x.0.clone())
.collect::<Vec<RedeemerPtr>>();
let missing: Vec<_> = redeemers_needed let missing: Vec<_> = redeemers_needed
.into_iter() .into_iter()
.filter(|x| !wits_rdptrs.contains(&x.0)) .filter(|x| !wits_redeemer_ptrs.contains(&x.0))
.map(|x| format!( .map(|x| {
"[Missing (rp: {:?}, sp: {:?}, sh: {})]", format!(
"[Missing (redeemer_ptr: {:?}, script_purpose: {:?}, script_hash: {})]",
x.0, x.0,
x.1, x.1,
x.2.to_string(), x.2.to_string(),
)) )
})
.collect(); .collect();
let extra: Vec<_> = wits_rdptrs let extra: Vec<_> = wits_redeemer_ptrs
.into_iter() .into_iter()
.filter(|x| !needed_rdptrs.contains(x)) .filter(|x| !needed_redeemer_ptrs.contains(x))
.map(|x| format!( .map(|x| format!("[Extraneous (redeemer_ptr: {:?})]", x))
"[Extraneous (rp: {:?})]",
x
))
.collect(); .collect();
if missing.len() > 0 || extra.len() > 0 { if missing.len() > 0 || extra.len() > 0 {
let missing_errors = missing.join(" "); let missing_errors = missing.join(" ");
let extra_errors = extra.join(" "); let extra_errors = extra.join(" ");
unreachable!("Mismatch in required redeemers: {} {}", missing_errors, extra_errors); Err(Error::RequiredRedeemersMismatch { missing, extra })
} } else {
Ok(()) Ok(())
} }
}
/// builds a redeemer pointer (tag, index) from a script purpose by setting the tag /// builds a redeemer pointer (tag, index) from a script purpose by setting the tag
/// according to the type of the script purpose, and the index according to the /// according to the type of the script purpose, and the index according to the
/// placement of script purpose inside its container. /// placement of script purpose inside its container.
fn rdptr( fn build_redeemer_ptr(
tx: &MintedTx, tx: &MintedTx,
sp: &ScriptPurpose, script_purpose: &ScriptPurpose,
) -> Option<RedeemerPtr> { ) -> Result<Option<RedeemerPtr>, Error> {
let txb = tx.transaction_body.clone(); let tx_body = tx.transaction_body.clone();
match sp { match script_purpose {
ScriptPurpose::Minting(hash) => { ScriptPurpose::Minting(hash) => {
let mut policy_ids = txb.mint let mut policy_ids = tx_body
.mint
.as_ref() .as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![])) .unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter() .iter()
@ -253,12 +247,15 @@ fn rdptr(
let maybe_idx = policy_ids.iter().position(|x| x == hash); let maybe_idx = policy_ids.iter().position(|x| x == hash);
match maybe_idx { match maybe_idx {
Some(idx) => Some(RedeemerPtr { tag: RedeemerTag::Mint, index: idx as u32 }), Some(idx) => Ok(Some(RedeemerPtr {
None => None, tag: RedeemerTag::Mint,
index: idx as u32,
})),
None => Ok(None),
} }
} }
ScriptPurpose::Spending(txin) => { ScriptPurpose::Spending(txin) => {
let mut inputs = txb.inputs.to_vec(); let mut inputs = tx_body.inputs.to_vec();
inputs.sort_by( inputs.sort_by(
|i_a, i_b| match i_a.transaction_id.cmp(&i_b.transaction_id) { |i_a, i_b| match i_a.transaction_id.cmp(&i_b.transaction_id) {
std::cmp::Ordering::Less => std::cmp::Ordering::Less, std::cmp::Ordering::Less => std::cmp::Ordering::Less,
@ -270,12 +267,16 @@ fn rdptr(
let maybe_idx = inputs.iter().position(|x| x == txin); let maybe_idx = inputs.iter().position(|x| x == txin);
match maybe_idx { match maybe_idx {
Some(idx) => Some(RedeemerPtr { tag: RedeemerTag::Spend, index: idx as u32 }), Some(idx) => Ok(Some(RedeemerPtr {
None => None, tag: RedeemerTag::Spend,
index: idx as u32,
})),
None => Ok(None),
}
} }
},
ScriptPurpose::Rewarding(racnt) => { ScriptPurpose::Rewarding(racnt) => {
let mut reward_accounts = txb.withdrawals let mut reward_accounts = tx_body
.withdrawals
.as_ref() .as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![])) .unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter() .iter()
@ -284,42 +285,47 @@ fn rdptr(
reward_accounts.sort(); reward_accounts.sort();
let maybe_idx = reward_accounts.iter().position(|x| { let mut maybe_idx = None;
for (idx, x) in reward_accounts.iter().enumerate() {
let cred = match Address::from_bytes(x).unwrap() { let cred = match Address::from_bytes(x).unwrap() {
Address::Stake(a) => match a.payload() { Address::Stake(a) => match a.payload() {
StakePayload::Script(sh) => { StakePayload::Script(sh) => StakeCredential::Scripthash(*sh),
StakeCredential::Scripthash(*sh)
}
StakePayload::Stake(_) => { StakePayload::Stake(_) => {
unreachable!( return Err(Error::ScriptKeyHash);
"This is impossible. A key hash cannot be the hash of a script."
);
} }
}, },
_ => unreachable!( _ => return Err(Error::BadWithdrawalAddress),
"This is impossible. Only shelley reward addresses can be a part of withdrawals."
),
}; };
cred == *racnt if cred == *racnt {
}); maybe_idx = Some(idx);
}
}
match maybe_idx { match maybe_idx {
Some(idx) => Some(RedeemerPtr { tag: RedeemerTag::Reward, index: idx as u32 }), Some(idx) => Ok(Some(RedeemerPtr {
None => None, tag: RedeemerTag::Reward,
index: idx as u32,
})),
None => Ok(None),
}
} }
},
ScriptPurpose::Certifying(d) => { ScriptPurpose::Certifying(d) => {
let maybe_idx = txb.certificates let maybe_idx = tx_body
.certificates
.as_ref() .as_ref()
.unwrap_or(&MaybeIndefArray::Indef(vec![])) .unwrap_or(&MaybeIndefArray::Indef(vec![]))
.iter() .iter()
.position(|x| x == d); .position(|x| x == d);
match maybe_idx { match maybe_idx {
Some(idx) => Some(RedeemerPtr{ tag: RedeemerTag::Cert, index: idx as u32 }), Some(idx) => Ok(Some(RedeemerPtr {
None => None tag: RedeemerTag::Cert,
} index: idx as u32,
}, })),
None => Ok(None),
}
}
} }
} }

676
crates/uplc/src/tx/tests.rs Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,16 +1,14 @@
use pallas_addresses::{Address, ShelleyDelegationPart, ShelleyPaymentPart}; use pallas_addresses::{Address, ShelleyDelegationPart, ShelleyPaymentPart};
use pallas_primitives::babbage::{AssetName, BigInt, Constr, PlutusData, ScriptRef};
use pallas_codec::utils::{AnyUInt, Bytes, Int, KeyValuePairs}; use pallas_codec::utils::{AnyUInt, Bytes, Int, KeyValuePairs};
use pallas_crypto::hash::Hash; use pallas_crypto::hash::Hash;
use pallas_primitives::babbage::{AssetName, BigInt, Constr, PlutusData, ScriptRef};
use pallas_primitives::babbage::{ use pallas_primitives::babbage::{
Certificate, DatumOption, PolicyId, Redeemer, Script, StakeCredential, TransactionInput, Certificate, DatumOption, PolicyId, Redeemer, Script, StakeCredential, TransactionInput,
TransactionOutput, Value, TransactionOutput, Value,
}; };
use pallas_traverse::ComputeHash; use pallas_traverse::ComputeHash;
use std::vec;
use super::script_context::{TxOut, TimeRange, TxInInfo, ScriptPurpose, TxInfo, ScriptContext}; use super::script_context::{ScriptContext, ScriptPurpose, TimeRange, TxInInfo, TxInfo, TxOut};
fn wrap_with_constr(index: u64, data: PlutusData) -> PlutusData { fn wrap_with_constr(index: u64, data: PlutusData) -> PlutusData {
PlutusData::Constr(Constr { PlutusData::Constr(Constr {