feat: implement script-related ledger checks for Tx Simulate (#57)

* feat: functions for extraneous/missing redeemers checks

* chore: typos

* feat: implement function to check for missing/extraneous scripts

* feat: check for missing/extraneous redeemers and scripts in eval_tx

* chore: add tests for missing/extraneous redeemers

* chore: remove duplicate file
This commit is contained in:
Harper
2022-09-19 04:31:30 +01:00
committed by GitHub
parent 9e280f9cb5
commit 6e901de2f0
3 changed files with 480 additions and 10 deletions

View File

@@ -9,7 +9,7 @@ use pallas_crypto::hash::Hash;
use pallas_primitives::babbage::{
Certificate, CostMdls, DatumHash, DatumOption, ExUnits, Language, Mint, MintedTx,
PlutusV1Script, PlutusV2Script, PolicyId, Redeemer, RedeemerTag, RewardAccount, Script,
StakeCredential, TransactionInput, TransactionOutput, Value, Withdrawals,
StakeCredential, TransactionInput, TransactionOutput, Value, Withdrawals, NativeScript,
};
use pallas_traverse::{ComputeHash, OriginalHash};
use std::{collections::HashMap, convert::TryInto, ops::Deref, vec};
@@ -40,7 +40,8 @@ fn slot_range_to_posix_time_range(slot_range: TimeRange, sc: &SlotConfig) -> Tim
}
#[derive(Debug, PartialEq, Clone)]
enum ScriptVersion {
pub enum ScriptVersion {
Native(NativeScript),
V1(PlutusV1Script),
V2(PlutusV2Script),
}
@@ -56,6 +57,12 @@ pub struct DataLookupTable {
scripts: HashMap<ScriptHash, ScriptVersion>,
}
impl DataLookupTable {
pub fn scripts(&self) -> HashMap<ScriptHash, ScriptVersion> {
self.scripts.clone()
}
}
pub fn get_tx_in_info_v1(
inputs: &[TransactionInput],
utxos: &[ResolvedInput],
@@ -184,15 +191,15 @@ fn get_script_purpose(
.as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter()
.map(|(policy_id, _)| policy_id.clone())
.map(|(racnt, _)| racnt.clone())
.collect::<Vec<RewardAccount>>();
reward_accounts.sort();
let reward_account = match reward_accounts.get(index as usize) {
Some(ra) => ra.clone(),
None => unreachable!("Script purpose not found for redeemer."),
};
let addresss = Address::from_bytes(&reward_account)?;
let credential = match addresss {
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)
@@ -501,6 +508,12 @@ pub fn get_script_and_datum_lookup_table(
.clone()
.unwrap_or_default();
let scripts_native_witnesses = tx
.transaction_witness_set
.native_script
.clone()
.unwrap_or_default();
let scripts_v1_witnesses = tx
.transaction_witness_set
.plutus_v1_script
@@ -517,14 +530,17 @@ pub fn get_script_and_datum_lookup_table(
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()));
// TODO: implement `original_hash` for native scripts in pallas
}
for script in scripts_v1_witnesses.iter() {
scripts.insert(script.compute_hash(), ScriptVersion::V1(script.clone()));
// TODO: fix hashing bug in pallas
}
for script in scripts_v2_witnesses.iter() {
scripts.insert(script.compute_hash(), ScriptVersion::V2(script.clone()));
// TODO: fix hashing bug in pallas
}
// discovery in utxos (script ref)
@@ -535,13 +551,15 @@ pub fn get_script_and_datum_lookup_table(
TransactionOutput::PostAlonzo(output) => {
if let Some(script) = &output.script_ref {
match &script.0 {
Script::NativeScript(ns) => {
scripts.insert(ns.compute_hash(), ScriptVersion::Native(ns.clone()));
}
Script::PlutusV1Script(v1) => {
scripts.insert(v1.compute_hash(), ScriptVersion::V1(v1.clone()));
}
Script::PlutusV2Script(v2) => {
scripts.insert(v2.compute_hash(), ScriptVersion::V2(v2.clone()));
}
_ => {}
}
}
}
@@ -663,6 +681,7 @@ pub fn eval_redeemer(
Ok(new_redeemer)
}
ScriptVersion::Native(_) => unreachable!("Native script can't be executed in phase-two.")
},
ExecutionPurpose::NoDatum(script_version) => match script_version {
ScriptVersion::V1(script) => {
@@ -755,6 +774,7 @@ pub fn eval_redeemer(
Ok(new_redeemer)
}
ScriptVersion::Native(_) => unreachable!("Native script can't be executed in phase-two.")
},
}
}

View File

@@ -0,0 +1,325 @@
use std::collections::HashMap;
use pallas_addresses::{ScriptHash, Address, ShelleyPaymentPart, StakePayload};
use pallas_codec::utils::{KeyValuePairs, MaybeIndefArray};
use pallas_primitives::babbage::{MintedTx, TransactionOutput, StakeCredential, Certificate, RedeemerTag, RewardAccount, PolicyId};
use super::{script_context::{ScriptPurpose, ResolvedInput}, eval::{ScriptVersion, DataLookupTable}};
// TODO: include in pallas eventually?
#[derive(Debug, PartialEq, Clone)]
struct RedeemerPtr {
tag: RedeemerTag,
index: u32,
}
type AlonzoScriptsNeeded = Vec<(ScriptPurpose, ScriptHash)>;
// subset of phase-1 ledger checks related to scripts
pub fn eval_phase_one(
tx: &MintedTx,
utxos: &[ResolvedInput],
lookup_table: &DataLookupTable,
) -> anyhow::Result<()> {
let scripts_needed = scripts_needed(tx, utxos);
validate_missing_scripts(&scripts_needed, lookup_table.scripts())?;
has_exact_set_of_redeemers(tx, &scripts_needed, lookup_table.scripts())?;
Ok(())
}
pub fn validate_missing_scripts(
needed: &AlonzoScriptsNeeded,
txscripts: HashMap<ScriptHash, ScriptVersion>,
) -> anyhow::Result<()> {
let received_hashes = txscripts
.keys()
.map(|x| *x)
.collect::<Vec<ScriptHash>>();
let needed_hashes = needed
.iter()
.map(|x| x.1)
.collect::<Vec<ScriptHash>>();
let missing: Vec<_> = needed_hashes
.clone()
.into_iter()
.filter(|x| !received_hashes.contains(x))
.map(|x| format!(
"[Missing (sh: {})]",
x
))
.collect();
let extra: Vec<_> = received_hashes
.into_iter()
.filter(|x| !needed_hashes.contains(x))
.map(|x| format!(
"[Extraneous (sh: {:?})]",
x
))
.collect();
if missing.len() > 0 || extra.len() > 0 {
let missing_errors = missing.join(" ");
let extra_errors = extra.join(" ");
unreachable!("Mismatch in required scripts: {} {}", missing_errors, extra_errors);
}
Ok(())
}
pub fn scripts_needed(
tx: &MintedTx,
utxos: &[ResolvedInput],
) -> AlonzoScriptsNeeded {
let mut needed = Vec::new();
let txb = tx.transaction_body.clone();
let mut spend = txb.inputs
.iter()
.map(|input| {
let utxo = match utxos.iter().find(|utxo| utxo.input == *input) {
Some(u) => u,
None => panic!("Resolved input not found."),
};
let address = Address::from_bytes(match &utxo.output {
TransactionOutput::Legacy(output) => output.address.as_ref(),
TransactionOutput::PostAlonzo(output) => output.address.as_ref(),
})
.unwrap();
if let Address::Shelley(a) = address {
if let ShelleyPaymentPart::Script(h) = a.payment() {
return Some((ScriptPurpose::Spending(input.clone()), *h))
}
}
None
})
.flatten()
.collect::<AlonzoScriptsNeeded>();
let mut reward = txb.withdrawals
.as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter()
.map(|(acnt, _)| {
let address = Address::from_bytes(acnt).unwrap();
if let Address::Stake(a) = address {
if let StakePayload::Script(h) = a.payload() {
let cred = StakeCredential::Scripthash(*h);
return Some((ScriptPurpose::Rewarding(cred), *h))
}
}
None
})
.flatten()
.collect::<AlonzoScriptsNeeded>();
let mut cert = txb.certificates
.clone()
.unwrap_or_default()
.iter()
.map(|cert| {
// only Dereg and Deleg certs can require scripts
match cert {
Certificate::StakeDeregistration(StakeCredential::Scripthash(h)) => {
Some((ScriptPurpose::Certifying(cert.clone()), *h))
},
Certificate::StakeDelegation(StakeCredential::Scripthash(h), _) => {
Some((ScriptPurpose::Certifying(cert.clone()), *h))
},
_ => None
}
})
.flatten()
.collect::<AlonzoScriptsNeeded>();
let mut mint = txb.mint
.as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter()
.map(|(policy_id, _)| {
(ScriptPurpose::Minting(*policy_id), *policy_id)
})
.collect::<AlonzoScriptsNeeded>();
needed.append(&mut spend);
needed.append(&mut reward);
needed.append(&mut cert);
needed.append(&mut mint);
needed
}
/// hasExactSetOfRedeemers in Ledger Spec, but we pass `txscripts` directly
pub fn has_exact_set_of_redeemers(
tx: &MintedTx,
needed: &AlonzoScriptsNeeded,
txscripts: HashMap<ScriptHash, ScriptVersion>,
) -> anyhow::Result<()> {
let redeemers_needed = needed
.iter()
.map(|(sp, sh)| {
let rp = rdptr(tx, sp);
let script = txscripts.get(&sh);
match (rp, script) {
(Some(ptr), Some(script)) => match script {
ScriptVersion::V1(_) => Some((ptr, sp.clone(), *sh)),
ScriptVersion::V2(_) => Some((ptr, sp.clone(), *sh)),
ScriptVersion::Native(_) => None,
},
_ => None
}
})
.flatten()
.collect::<Vec<(RedeemerPtr, ScriptPurpose, ScriptHash)>>();
let wits_rdptrs = tx
.transaction_witness_set
.redeemer
.as_ref()
.unwrap_or(&MaybeIndefArray::Indef(vec![]))
.iter()
.map(|r| {
RedeemerPtr { tag: r.tag.clone(), index: r.index }
})
.collect::<Vec<RedeemerPtr>>();
let needed_rdptrs = redeemers_needed
.iter()
.map(|x| x.0.clone())
.collect::<Vec<RedeemerPtr>>();
let missing: Vec<_> = redeemers_needed
.into_iter()
.filter(|x| !wits_rdptrs.contains(&x.0))
.map(|x| format!(
"[Missing (rp: {:?}, sp: {:?}, sh: {})]",
x.0,
x.1,
x.2.to_string(),
))
.collect();
let extra: Vec<_> = wits_rdptrs
.into_iter()
.filter(|x| !needed_rdptrs.contains(x))
.map(|x| format!(
"[Extraneous (rp: {:?})]",
x
))
.collect();
if missing.len() > 0 || extra.len() > 0 {
let missing_errors = missing.join(" ");
let extra_errors = extra.join(" ");
unreachable!("Mismatch in required redeemers: {} {}", missing_errors, extra_errors);
}
Ok(())
}
/// 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
/// placement of script purpose inside its container.
fn rdptr(
tx: &MintedTx,
sp: &ScriptPurpose,
) -> Option<RedeemerPtr> {
let txb = tx.transaction_body.clone();
match sp {
ScriptPurpose::Minting(hash) => {
let mut policy_ids = txb.mint
.as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter()
.map(|(policy_id, _)| *policy_id)
.collect::<Vec<PolicyId>>();
policy_ids.sort();
let maybe_idx = policy_ids.iter().position(|x| x == hash);
match maybe_idx {
Some(idx) => Some(RedeemerPtr { tag: RedeemerTag::Mint, index: idx as u32 }),
None => None,
}
}
ScriptPurpose::Spending(txin) => {
let mut inputs = txb.inputs.to_vec();
inputs.sort_by(
|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::Equal => i_a.index.cmp(&i_b.index),
std::cmp::Ordering::Greater => std::cmp::Ordering::Greater,
},
);
let maybe_idx = inputs.iter().position(|x| x == txin);
match maybe_idx {
Some(idx) => Some(RedeemerPtr { tag: RedeemerTag::Spend, index: idx as u32 }),
None => None,
}
},
ScriptPurpose::Rewarding(racnt) => {
let mut reward_accounts = txb.withdrawals
.as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter()
.map(|(acnt, _)| acnt.clone())
.collect::<Vec<RewardAccount>>();
reward_accounts.sort();
let maybe_idx = reward_accounts.iter().position(|x| {
let cred = match Address::from_bytes(x).unwrap() {
Address::Stake(a) => match a.payload() {
StakePayload::Script(sh) => {
StakeCredential::Scripthash(*sh)
}
StakePayload::Stake(_) => {
unreachable!(
"This is impossible. A key hash cannot be the hash of a script."
);
}
},
_ => unreachable!(
"This is impossible. Only shelley reward addresses can be a part of withdrawals."
),
};
cred == *racnt
});
match maybe_idx {
Some(idx) => Some(RedeemerPtr { tag: RedeemerTag::Reward, index: idx as u32 }),
None => None,
}
},
ScriptPurpose::Certifying(d) => {
let maybe_idx = txb.certificates
.as_ref()
.unwrap_or(&MaybeIndefArray::Indef(vec![]))
.iter()
.position(|x| x == d);
match maybe_idx {
Some(idx) => Some(RedeemerPtr{ tag: RedeemerTag::Cert, index: idx as u32 }),
None => None
}
},
}
}