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:
parent
9e280f9cb5
commit
6e901de2f0
|
@ -4,13 +4,15 @@ use pallas_primitives::{
|
||||||
};
|
};
|
||||||
use pallas_traverse::{Era, MultiEraTx};
|
use pallas_traverse::{Era, MultiEraTx};
|
||||||
|
|
||||||
use error::Error;
|
use crate::Error;
|
||||||
use script_context::{ResolvedInput, SlotConfig};
|
|
||||||
|
use self::{script_context::{ResolvedInput, SlotConfig}, phase_one::eval_phase_one};
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod eval;
|
mod eval;
|
||||||
pub mod script_context;
|
pub mod script_context;
|
||||||
mod to_plutus_data;
|
mod to_plutus_data;
|
||||||
|
mod phase_one;
|
||||||
|
|
||||||
pub fn eval(
|
pub fn eval(
|
||||||
tx: &MintedTx,
|
tx: &MintedTx,
|
||||||
|
@ -22,6 +24,9 @@ pub fn eval(
|
||||||
|
|
||||||
let lookup_table = eval::get_script_and_datum_lookup_table(tx, utxos);
|
let lookup_table = eval::get_script_and_datum_lookup_table(tx, utxos);
|
||||||
|
|
||||||
|
// subset of phase 1 check on redeemers and scripts
|
||||||
|
eval_phase_one(tx, utxos, &lookup_table)?;
|
||||||
|
|
||||||
match redeemers {
|
match redeemers {
|
||||||
Some(rs) => {
|
Some(rs) => {
|
||||||
let mut collected_redeemers = vec![];
|
let mut collected_redeemers = vec![];
|
||||||
|
@ -633,4 +638,124 @@ mod tests {
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eval_missing_redeemer() {
|
||||||
|
let tx_bytes = hex::decode("84a30082825820275b5da338c8b899035081eb34bfa950b634911a5dd3271b3ad6cf4c2bba0c5000825820275b5da338c8b899035081eb34bfa950b634911a5dd3271b3ad6cf4c2bba0c50010181825839000af00cc47500bb64cfffb783e8c42f746b4e8b8a70ede9c08c7113acf3bde34d1041f5a2076ef9aa6cf4539ab1a96ed462a0300acbdb65d51a02cf47c8021a00028d89a1068149480100002221200101f5f6").unwrap();
|
||||||
|
|
||||||
|
let multi_era_tx = MultiEraTx::decode(Era::Babbage, &tx_bytes)
|
||||||
|
.or_else(|_| MultiEraTx::decode(Era::Alonzo, &tx_bytes))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let inputs = multi_era_tx.as_babbage().unwrap().transaction_body.inputs.clone();
|
||||||
|
|
||||||
|
let raw_outputs = hex::decode("82825839000af00cc47500bb64cfffb783e8c42f746b4e8b8a70ede9c08c7113acf3bde34d1041f5a2076ef9aa6cf4539ab1a96ed462a0300acbdb65d51a02b3603082581d703a888d65f16790950a72daee1f63aa05add6d268434107cfa5b677121a001e8480").unwrap();
|
||||||
|
|
||||||
|
let outputs = MaybeIndefArray::<TransactionOutput>::decode_fragment(&raw_outputs).unwrap();
|
||||||
|
|
||||||
|
let utxos: MaybeIndefArray<ResolvedInput> = MaybeIndefArray::Indef(
|
||||||
|
inputs
|
||||||
|
.iter()
|
||||||
|
.zip(outputs.iter())
|
||||||
|
.map(|(input, output)| ResolvedInput {
|
||||||
|
input: input.clone(),
|
||||||
|
output: output.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let slot_config = SlotConfig {
|
||||||
|
zero_time: 1660003200000, // Preview network
|
||||||
|
slot_length: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
let costs: Vec<i64> = vec![
|
||||||
|
205665, 812, 1, 1, 1000, 571, 0, 1, 1000, 24177, 4, 1, 1000, 32, 117366, 10475, 4,
|
||||||
|
23000, 100, 23000, 100, 23000, 100, 23000, 100, 23000, 100, 23000, 100, 100, 100,
|
||||||
|
23000, 100, 19537, 32, 175354, 32, 46417, 4, 221973, 511, 0, 1, 89141, 32, 497525,
|
||||||
|
14068, 4, 2, 196500, 453240, 220, 0, 1, 1, 1000, 28662, 4, 2, 245000, 216773, 62, 1,
|
||||||
|
1060367, 12586, 1, 208512, 421, 1, 187000, 1000, 52998, 1, 80436, 32, 43249, 32, 1000,
|
||||||
|
32, 80556, 1, 57667, 4, 1000, 10, 197145, 156, 1, 197145, 156, 1, 204924, 473, 1,
|
||||||
|
208896, 511, 1, 52467, 32, 64832, 32, 65493, 32, 22558, 32, 16563, 32, 76511, 32,
|
||||||
|
196500, 453240, 220, 0, 1, 1, 69522, 11687, 0, 1, 60091, 32, 196500, 453240, 220, 0, 1,
|
||||||
|
1, 196500, 453240, 220, 0, 1, 1, 806990, 30482, 4, 1927926, 82523, 4, 265318, 0, 4, 0,
|
||||||
|
85931, 32, 205665, 812, 1, 1, 41182, 32, 212342, 32, 31220, 32, 32696, 32, 43357, 32,
|
||||||
|
32247, 32, 38314, 32, 9462713, 1021, 10,
|
||||||
|
];
|
||||||
|
|
||||||
|
let cost_mdl = CostMdls {
|
||||||
|
plutus_v1: Some(costs),
|
||||||
|
plutus_v2: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let multi_era_tx = MultiEraTx::decode(Era::Babbage, &tx_bytes)
|
||||||
|
.or_else(|_| MultiEraTx::decode(Era::Alonzo, &tx_bytes))
|
||||||
|
.unwrap();
|
||||||
|
match multi_era_tx {
|
||||||
|
MultiEraTx::Babbage(tx) => {
|
||||||
|
eval(&tx, &utxos, Some(&cost_mdl), &slot_config).unwrap();
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eval_extraneous_redeemer() {
|
||||||
|
let tx_bytes = hex::decode("84a70082825820275b5da338c8b899035081eb34bfa950b634911a5dd3271b3ad6cf4c2bba0c5000825820275b5da338c8b899035081eb34bfa950b634911a5dd3271b3ad6cf4c2bba0c50010181825839000af00cc47500bb64cfffb783e8c42f746b4e8b8a70ede9c08c7113acf3bde34d1041f5a2076ef9aa6cf4539ab1a96ed462a0300acbdb65d51a02cf2b47021a0002aa0a0b5820fc54f302cff3a8a1cb374f5e4979e18a1d3627dcf4539637b03f5959eb8565bf0d81825820275b5da338c8b899035081eb34bfa950b634911a5dd3271b3ad6cf4c2bba0c500110825839000af00cc47500bb64cfffb783e8c42f746b4e8b8a70ede9c08c7113acf3bde34d1041f5a2076ef9aa6cf4539ab1a96ed462a0300acbdb65d51a02af51c2111a0003ff0fa40081825820065dd553fbe4e240a8f819bb9e333a7483de4a22b65c7fb6a95ce9450f84dff758402c26125a057a696079d08f2c8c9d2b8ccda9fe7cf7360c1a86712b85a91db82a3b80996b30ba6f4b2f969c93eb50694e0f6ea0bcf129080dcc07ecd9e605f00a049fd87980ff0582840000d879808219044c1a000382d48401001864821903e81903e8068149480100002221200101f5f6").unwrap();
|
||||||
|
|
||||||
|
let multi_era_tx = MultiEraTx::decode(Era::Babbage, &tx_bytes)
|
||||||
|
.or_else(|_| MultiEraTx::decode(Era::Alonzo, &tx_bytes))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let inputs = multi_era_tx.as_babbage().unwrap().transaction_body.inputs.clone();
|
||||||
|
|
||||||
|
let raw_outputs = hex::decode("82825839000af00cc47500bb64cfffb783e8c42f746b4e8b8a70ede9c08c7113acf3bde34d1041f5a2076ef9aa6cf4539ab1a96ed462a0300acbdb65d51a02b3603082581d703a888d65f16790950a72daee1f63aa05add6d268434107cfa5b677121a001e8480").unwrap();
|
||||||
|
|
||||||
|
let outputs = MaybeIndefArray::<TransactionOutput>::decode_fragment(&raw_outputs).unwrap();
|
||||||
|
|
||||||
|
let utxos: MaybeIndefArray<ResolvedInput> = MaybeIndefArray::Indef(
|
||||||
|
inputs
|
||||||
|
.iter()
|
||||||
|
.zip(outputs.iter())
|
||||||
|
.map(|(input, output)| ResolvedInput {
|
||||||
|
input: input.clone(),
|
||||||
|
output: output.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let slot_config = SlotConfig {
|
||||||
|
zero_time: 1660003200000, // Preview network
|
||||||
|
slot_length: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
let costs: Vec<i64> = vec![
|
||||||
|
205665, 812, 1, 1, 1000, 571, 0, 1, 1000, 24177, 4, 1, 1000, 32, 117366, 10475, 4,
|
||||||
|
23000, 100, 23000, 100, 23000, 100, 23000, 100, 23000, 100, 23000, 100, 100, 100,
|
||||||
|
23000, 100, 19537, 32, 175354, 32, 46417, 4, 221973, 511, 0, 1, 89141, 32, 497525,
|
||||||
|
14068, 4, 2, 196500, 453240, 220, 0, 1, 1, 1000, 28662, 4, 2, 245000, 216773, 62, 1,
|
||||||
|
1060367, 12586, 1, 208512, 421, 1, 187000, 1000, 52998, 1, 80436, 32, 43249, 32, 1000,
|
||||||
|
32, 80556, 1, 57667, 4, 1000, 10, 197145, 156, 1, 197145, 156, 1, 204924, 473, 1,
|
||||||
|
208896, 511, 1, 52467, 32, 64832, 32, 65493, 32, 22558, 32, 16563, 32, 76511, 32,
|
||||||
|
196500, 453240, 220, 0, 1, 1, 69522, 11687, 0, 1, 60091, 32, 196500, 453240, 220, 0, 1,
|
||||||
|
1, 196500, 453240, 220, 0, 1, 1, 806990, 30482, 4, 1927926, 82523, 4, 265318, 0, 4, 0,
|
||||||
|
85931, 32, 205665, 812, 1, 1, 41182, 32, 212342, 32, 31220, 32, 32696, 32, 43357, 32,
|
||||||
|
32247, 32, 38314, 32, 9462713, 1021, 10,
|
||||||
|
];
|
||||||
|
|
||||||
|
let cost_mdl = CostMdls {
|
||||||
|
plutus_v1: Some(costs),
|
||||||
|
plutus_v2: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let multi_era_tx = MultiEraTx::decode(Era::Babbage, &tx_bytes)
|
||||||
|
.or_else(|_| MultiEraTx::decode(Era::Alonzo, &tx_bytes))
|
||||||
|
.unwrap();
|
||||||
|
match multi_era_tx {
|
||||||
|
MultiEraTx::Babbage(tx) => {
|
||||||
|
eval(&tx, &utxos, Some(&cost_mdl), &slot_config).unwrap();
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ use pallas_crypto::hash::Hash;
|
||||||
use pallas_primitives::babbage::{
|
use pallas_primitives::babbage::{
|
||||||
Certificate, CostMdls, DatumHash, DatumOption, ExUnits, Language, Mint, MintedTx,
|
Certificate, CostMdls, DatumHash, DatumOption, ExUnits, Language, Mint, MintedTx,
|
||||||
PlutusV1Script, PlutusV2Script, PolicyId, Redeemer, RedeemerTag, RewardAccount, Script,
|
PlutusV1Script, PlutusV2Script, PolicyId, Redeemer, RedeemerTag, RewardAccount, Script,
|
||||||
StakeCredential, TransactionInput, TransactionOutput, Value, Withdrawals,
|
StakeCredential, TransactionInput, TransactionOutput, Value, Withdrawals, NativeScript,
|
||||||
};
|
};
|
||||||
use pallas_traverse::{ComputeHash, OriginalHash};
|
use pallas_traverse::{ComputeHash, OriginalHash};
|
||||||
use std::{collections::HashMap, convert::TryInto, ops::Deref, vec};
|
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)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
enum ScriptVersion {
|
pub enum ScriptVersion {
|
||||||
|
Native(NativeScript),
|
||||||
V1(PlutusV1Script),
|
V1(PlutusV1Script),
|
||||||
V2(PlutusV2Script),
|
V2(PlutusV2Script),
|
||||||
}
|
}
|
||||||
|
@ -56,6 +57,12 @@ pub struct DataLookupTable {
|
||||||
scripts: HashMap<ScriptHash, ScriptVersion>,
|
scripts: HashMap<ScriptHash, ScriptVersion>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DataLookupTable {
|
||||||
|
pub fn scripts(&self) -> HashMap<ScriptHash, ScriptVersion> {
|
||||||
|
self.scripts.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_tx_in_info_v1(
|
pub fn get_tx_in_info_v1(
|
||||||
inputs: &[TransactionInput],
|
inputs: &[TransactionInput],
|
||||||
utxos: &[ResolvedInput],
|
utxos: &[ResolvedInput],
|
||||||
|
@ -184,15 +191,15 @@ fn get_script_purpose(
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap_or(&KeyValuePairs::Indef(vec![]))
|
.unwrap_or(&KeyValuePairs::Indef(vec![]))
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(policy_id, _)| policy_id.clone())
|
.map(|(racnt, _)| racnt.clone())
|
||||||
.collect::<Vec<RewardAccount>>();
|
.collect::<Vec<RewardAccount>>();
|
||||||
reward_accounts.sort();
|
reward_accounts.sort();
|
||||||
let reward_account = match reward_accounts.get(index as usize) {
|
let reward_account = match reward_accounts.get(index as usize) {
|
||||||
Some(ra) => ra.clone(),
|
Some(ra) => ra.clone(),
|
||||||
None => unreachable!("Script purpose not found for redeemer."),
|
None => unreachable!("Script purpose not found for redeemer."),
|
||||||
};
|
};
|
||||||
let addresss = Address::from_bytes(&reward_account)?;
|
let address = Address::from_bytes(&reward_account)?;
|
||||||
let credential = match addresss {
|
let credential = match address {
|
||||||
Address::Stake(stake_address) => match stake_address.payload() {
|
Address::Stake(stake_address) => match stake_address.payload() {
|
||||||
StakePayload::Script(script_hash) => {
|
StakePayload::Script(script_hash) => {
|
||||||
StakeCredential::Scripthash(*script_hash)
|
StakeCredential::Scripthash(*script_hash)
|
||||||
|
@ -501,6 +508,12 @@ pub fn get_script_and_datum_lookup_table(
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let scripts_native_witnesses = tx
|
||||||
|
.transaction_witness_set
|
||||||
|
.native_script
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let scripts_v1_witnesses = tx
|
let scripts_v1_witnesses = tx
|
||||||
.transaction_witness_set
|
.transaction_witness_set
|
||||||
.plutus_v1_script
|
.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());
|
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() {
|
for script in scripts_v1_witnesses.iter() {
|
||||||
scripts.insert(script.compute_hash(), ScriptVersion::V1(script.clone()));
|
scripts.insert(script.compute_hash(), ScriptVersion::V1(script.clone()));
|
||||||
// TODO: fix hashing bug in pallas
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for script in scripts_v2_witnesses.iter() {
|
for script in scripts_v2_witnesses.iter() {
|
||||||
scripts.insert(script.compute_hash(), ScriptVersion::V2(script.clone()));
|
scripts.insert(script.compute_hash(), ScriptVersion::V2(script.clone()));
|
||||||
// TODO: fix hashing bug in pallas
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// discovery in utxos (script ref)
|
// discovery in utxos (script ref)
|
||||||
|
@ -535,13 +551,15 @@ pub fn get_script_and_datum_lookup_table(
|
||||||
TransactionOutput::PostAlonzo(output) => {
|
TransactionOutput::PostAlonzo(output) => {
|
||||||
if let Some(script) = &output.script_ref {
|
if let Some(script) = &output.script_ref {
|
||||||
match &script.0 {
|
match &script.0 {
|
||||||
|
Script::NativeScript(ns) => {
|
||||||
|
scripts.insert(ns.compute_hash(), ScriptVersion::Native(ns.clone()));
|
||||||
|
}
|
||||||
Script::PlutusV1Script(v1) => {
|
Script::PlutusV1Script(v1) => {
|
||||||
scripts.insert(v1.compute_hash(), ScriptVersion::V1(v1.clone()));
|
scripts.insert(v1.compute_hash(), ScriptVersion::V1(v1.clone()));
|
||||||
}
|
}
|
||||||
Script::PlutusV2Script(v2) => {
|
Script::PlutusV2Script(v2) => {
|
||||||
scripts.insert(v2.compute_hash(), ScriptVersion::V2(v2.clone()));
|
scripts.insert(v2.compute_hash(), ScriptVersion::V2(v2.clone()));
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -663,6 +681,7 @@ pub fn eval_redeemer(
|
||||||
|
|
||||||
Ok(new_redeemer)
|
Ok(new_redeemer)
|
||||||
}
|
}
|
||||||
|
ScriptVersion::Native(_) => unreachable!("Native script can't be executed in phase-two.")
|
||||||
},
|
},
|
||||||
ExecutionPurpose::NoDatum(script_version) => match script_version {
|
ExecutionPurpose::NoDatum(script_version) => match script_version {
|
||||||
ScriptVersion::V1(script) => {
|
ScriptVersion::V1(script) => {
|
||||||
|
@ -755,6 +774,7 @@ pub fn eval_redeemer(
|
||||||
|
|
||||||
Ok(new_redeemer)
|
Ok(new_redeemer)
|
||||||
}
|
}
|
||||||
|
ScriptVersion::Native(_) => unreachable!("Native script can't be executed in phase-two.")
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue