Fix script context translations for withdrawals and validity intervals.

This commit is contained in:
KtorZ
2024-08-13 23:43:47 +02:00
parent fe5c5650a1
commit f879f6d183
27 changed files with 1674 additions and 258 deletions

View File

@@ -9,7 +9,7 @@ use std::{fmt, fs, path::PathBuf, process};
use uplc::{
machine::cost_model::ExBudget,
tx::{
self,
self, redeemer_tag_to_string,
script_context::{ResolvedInput, SlotConfig},
},
};
@@ -110,11 +110,11 @@ pub fn exec(
let with_redeemer = |redeemer: &Redeemer| {
eprintln!(
"{} {:?}[{}]",
"{} {}[{}]",
" Evaluating"
.if_supports_color(Stderr, |s| s.purple())
.if_supports_color(Stderr, |s| s.bold()),
redeemer.tag,
redeemer_tag_to_string(&redeemer.tag),
redeemer.index
)
};

View File

@@ -16,6 +16,7 @@ pub use pallas_primitives::{
babbage::{PostAlonzoTransactionOutput, TransactionInput, TransactionOutput, Value},
Error, Fragment,
};
pub use tx::redeemer_tag_to_string;
pub fn plutus_data(bytes: &[u8]) -> Result<PlutusData, Error> {
PlutusData::decode_fragment(bytes)

View File

@@ -13,9 +13,19 @@ pub enum Error {
OpenTermEvaluated(Term<NamedDeBruijn>),
#[error("The validator crashed / exited prematurely")]
EvaluationFailure,
#[error("Attempted to instantiate a non-polymorphic term:\n\n{0:#?}")]
#[error(
"Attempted to instantiate a non-polymorphic term\n{:>13} {}",
"Term",
indent(redacted(format!("{:#?}", .0), 10)),
)]
NonPolymorphicInstantiation(Value),
#[error("Attempted to apply a non-function:\n\n{0:#?} to argument:\n\n{1:#?}")]
#[error(
"Attempted to apply an argument to a non-function\n{:>13} {}\n{:>13} {}",
"Thing",
indent(redacted(format!("{:#?}", .0), 5)),
"Argument",
indent(redacted(format!("{:#?}", .1), 5)),
)]
NonFunctionalApplication(Value, Value),
#[error("Attempted to case a non-const:\n\n{0:#?}")]
NonConstrScrutinized(Value),
@@ -61,7 +71,10 @@ pub enum Error {
UnexpectedEd25519PublicKeyLength(usize),
#[error("Ed25519S Signature should be 64 bytes but it was {0}")]
UnexpectedEd25519SignatureLength(usize),
#[error("Failed to deserialise PlutusData using {0}:\n\n{1:#?}")]
#[error(
"Failed to deserialise PlutusData using {0}:\n\n{}",
redacted(format!("{:#?}", .1), 10),
)]
DeserialisationError(String, Value),
#[error("Integer overflow")]
OverflowError,
@@ -83,3 +96,21 @@ impl From<k256::ecdsa::Error> for Error {
Error::K256Error(format!("K256 error: {}", error))
}
}
/// Print only the first n lines of possibly long output, and redact the rest if longer.
fn redacted(s: String, max_rows: usize) -> String {
let rows = s.lines();
if rows.count() > max_rows {
let last_line = s.lines().last().unwrap();
let mut s = s.lines().take(max_rows).collect::<Vec<_>>().join("\n");
s.push_str(&format!("\n ...redacted...\n{last_line}"));
s
} else {
s
}
}
fn indent(s: String) -> String {
s.lines().collect::<Vec<_>>().join(&format!("\n{:>14}", ""))
}

View File

@@ -9,7 +9,7 @@ use pallas_primitives::{
Fragment,
};
use pallas_traverse::{Era, MultiEraTx};
pub use phase_one::eval_phase_one;
pub use phase_one::{eval_phase_one, redeemer_tag_to_string};
pub use script_context::{DataLookupTable, ResolvedInput, SlotConfig};
pub mod error;

View File

@@ -99,4 +99,6 @@ pub enum Error {
MissingScriptForRedeemer,
#[error("failed to apply parameters to Plutus script")]
ApplyParamsError,
#[error("validity start or end too far in the past")]
SlotTooFarInThePast { oldest_allowed: u64 },
}

View File

@@ -4,15 +4,16 @@ use super::{
Error,
};
use crate::{
ast::{Data, FakeNamedDeBruijn, NamedDeBruijn, Program},
ast::{FakeNamedDeBruijn, NamedDeBruijn, Program},
machine::cost_model::ExBudget,
tx::script_context::{DataLookupTable, ScriptVersion, TxInfoV1, TxInfoV2, TxInfoV3},
tx::{
phase_one::redeemer_tag_to_string,
script_context::{DataLookupTable, ScriptVersion, TxInfoV1, TxInfoV2, TxInfoV3},
},
PlutusData,
};
use pallas_codec::utils::Bytes;
use pallas_primitives::conway::{
CostMdls, CostModel, ExUnits, Language, MintedTx, Redeemer, RedeemerTag,
};
use pallas_primitives::conway::{CostMdls, CostModel, ExUnits, Language, MintedTx, Redeemer};
pub fn eval_redeemer(
tx: &MintedTx,
@@ -44,21 +45,16 @@ pub fn eval_redeemer(
}
.apply_data(redeemer.data.clone())
.apply_data(script_context.to_plutus_data()),
ScriptContext::V3 { .. } if datum.is_some() => {
// FIXME: Temporary, but needed until https://github.com/aiken-lang/aiken/pull/977
// is implemented.
ScriptContext::V3 { .. } => if let Some(datum) = datum {
program.apply_data(datum)
} else {
program
// FIXME: Temporary, but needed until https://github.com/aiken-lang/aiken/pull/977
// is implemented.
.apply_data(Data::constr(0, vec![]))
.apply_data(Data::constr(0, vec![]))
.apply_data(script_context.to_plutus_data())
}
ScriptContext::V3 { .. } => {
program
// FIXME: Temporary, but needed until https://github.com/aiken-lang/aiken/pull/977
// is implemented.
.apply_data(Data::constr(0, vec![]))
.apply_data(script_context.to_plutus_data())
}
.apply_data(redeemer.data.clone())
.apply_data(script_context.to_plutus_data()),
};
let mut eval_result = if let Some(costs) = cost_mdl_opt {
@@ -154,15 +150,3 @@ pub fn eval_redeemer(
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()
}

View File

@@ -2,6 +2,7 @@ use super::{
error::Error,
script_context::{sort_voters, DataLookupTable, ResolvedInput, ScriptPurpose, ScriptVersion},
};
use crate::tx::script_context::sort_reward_accounts;
use itertools::Itertools;
use pallas_addresses::{Address, ScriptHash, ShelleyPaymentPart, StakePayload};
use pallas_codec::utils::Nullable;
@@ -234,13 +235,20 @@ pub fn has_exact_set_of_redeemers(
let missing: Vec<_> = redeemers_needed
.into_iter()
.filter(|x| !wits_redeemer_keys.contains(&&x.0))
.map(|x| format!("{:?}[{:?}] -> {}", x.0.tag, x.0.index, x.2))
.map(|x| {
format!(
"{}[{:?}] -> {}",
redeemer_tag_to_string(&x.0.tag),
x.0.index,
x.2
)
})
.collect();
let extra: Vec<_> = wits_redeemer_keys
.into_iter()
.filter(|x| !needed_redeemer_keys.contains(x))
.map(|x| format!("{:?}[{:?}]", x.tag, x.index))
.map(|x| format!("{}[{:?}]", redeemer_tag_to_string(&x.tag), x.index))
.collect();
if !missing.is_empty() || !extra.is_empty() {
@@ -306,7 +314,7 @@ fn build_redeemer_key(
.map(|m| m.iter().map(|(acnt, _)| acnt).collect())
.unwrap_or_default();
reward_accounts.sort();
reward_accounts.sort_by(|acnt_a, acnt_b| sort_reward_accounts(acnt_a, acnt_b));
let mut redeemer_key = None;
@@ -377,3 +385,15 @@ fn build_redeemer_key(
}
}
}
pub 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()
}

View File

@@ -1,8 +1,8 @@
use super::{to_plutus_data::MintValue, Error};
use itertools::Itertools;
use pallas_addresses::{Address, StakePayload};
use pallas_addresses::{Address, Network, StakePayload};
use pallas_codec::utils::{
KeyValuePairs, NonEmptyKeyValuePairs, NonEmptySet, Nullable, PositiveCoin,
Bytes, KeyValuePairs, NonEmptyKeyValuePairs, NonEmptySet, Nullable, PositiveCoin,
};
use pallas_crypto::hash::Hash;
use pallas_primitives::{
@@ -217,7 +217,7 @@ impl TxInfoV1 {
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));
KeyValuePairs::from(get_withdrawals_info(&tx.transaction_body.withdrawals));
let mint = get_mint_info(&tx.transaction_body.mint);
let redeemers = get_redeemers_info(
@@ -232,7 +232,7 @@ impl TxInfoV1 {
mint,
certificates,
withdrawals: withdrawals.into(),
valid_range: get_validity_range_info(&tx.transaction_body, slot_config),
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,
@@ -266,7 +266,7 @@ impl TxInfoV2 {
let inputs = get_tx_in_info_v2(&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));
KeyValuePairs::from(get_withdrawals_info(&tx.transaction_body.withdrawals));
let mint = get_mint_info(&tx.transaction_body.mint);
let redeemers = get_redeemers_info(
@@ -290,7 +290,7 @@ impl TxInfoV2 {
mint,
certificates,
withdrawals,
valid_range: get_validity_range_info(&tx.transaction_body, slot_config),
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)),
redeemers,
@@ -330,7 +330,7 @@ impl TxInfoV3 {
let certificates = get_certificates_info(&tx.transaction_body.certificates);
let withdrawals =
KeyValuePairs::from(get_withdrawal_info(&tx.transaction_body.withdrawals));
KeyValuePairs::from(get_withdrawals_info(&tx.transaction_body.withdrawals));
let mint = get_mint_info(&tx.transaction_body.mint);
@@ -367,7 +367,7 @@ impl TxInfoV3 {
mint,
certificates,
withdrawals,
valid_range: get_validity_range_info(&tx.transaction_body, slot_config),
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)),
redeemers,
@@ -626,14 +626,14 @@ pub fn get_proposal_procedures_info(
.unwrap_or_default()
}
pub fn get_withdrawal_info(
pub fn get_withdrawals_info(
withdrawals: &Option<NonEmptyKeyValuePairs<RewardAccount, Coin>>,
) -> Vec<(Address, Coin)> {
withdrawals
.clone()
.map(|w| {
w.into_iter()
.sorted()
.sorted_by(|(accnt_a, _), (accnt_b, _)| sort_reward_accounts(accnt_a, accnt_b))
.map(|(reward_account, coin)| (Address::from_bytes(&reward_account).unwrap(), coin))
.collect()
})
@@ -643,21 +643,31 @@ pub fn get_withdrawal_info(
pub fn get_validity_range_info(
body: &MintedTransactionBody,
slot_config: &SlotConfig,
) -> TimeRange {
fn slot_to_begin_posix_time(slot: u64, sc: &SlotConfig) -> u64 {
) -> Result<TimeRange, Error> {
fn slot_to_begin_posix_time(slot: u64, sc: &SlotConfig) -> Result<u64, Error> {
if slot < sc.zero_slot {
return Err(Error::SlotTooFarInThePast {
oldest_allowed: sc.zero_slot,
});
}
let ms_after_begin = (slot - sc.zero_slot) * sc.slot_length as u64;
sc.zero_time + ms_after_begin
Ok(sc.zero_time + ms_after_begin)
}
fn slot_range_to_posix_time_range(slot_range: TimeRange, sc: &SlotConfig) -> TimeRange {
TimeRange {
fn slot_range_to_posix_time_range(
slot_range: TimeRange,
sc: &SlotConfig,
) -> Result<TimeRange, Error> {
Ok(TimeRange {
lower_bound: slot_range
.lower_bound
.map(|lower_bound| slot_to_begin_posix_time(lower_bound, sc)),
.map(|lower_bound| slot_to_begin_posix_time(lower_bound, sc))
.transpose()?,
upper_bound: slot_range
.upper_bound
.map(|upper_bound| slot_to_begin_posix_time(upper_bound, sc)),
}
.map(|upper_bound| slot_to_begin_posix_time(upper_bound, sc))
.transpose()?,
})
}
slot_range_to_posix_time_range(
@@ -841,7 +851,7 @@ pub fn find_script(
lookup_script(&hash)
}),
RedeemerTag::Reward => get_withdrawal_info(&tx.transaction_body.withdrawals)
RedeemerTag::Reward => get_withdrawals_info(&tx.transaction_body.withdrawals)
.get(redeemer.index as usize)
.ok_or(Error::MissingScriptForRedeemer)
.and_then(|(addr, _)| {
@@ -1086,6 +1096,34 @@ fn sort_gov_action_id(a: &GovActionId, b: &GovActionId) -> Ordering {
}
}
pub fn sort_reward_accounts(a: &Bytes, b: &Bytes) -> Ordering {
let addr_a = Address::from_bytes(a).expect("invalid reward address in withdrawals.");
let addr_b = Address::from_bytes(b).expect("invalid reward address in withdrawals.");
fn network_tag(network: Network) -> u8 {
match network {
Network::Testnet => 0,
Network::Mainnet => 1,
Network::Other(tag) => tag,
}
}
if let (Address::Stake(accnt_a), Address::Stake(accnt_b)) = (addr_a, addr_b) {
if accnt_a.network() != accnt_b.network() {
return network_tag(accnt_a.network()).cmp(&network_tag(accnt_b.network()));
}
match (accnt_a.payload(), accnt_b.payload()) {
(StakePayload::Script(..), StakePayload::Stake(..)) => Ordering::Less,
(StakePayload::Stake(..), StakePayload::Script(..)) => Ordering::Greater,
(StakePayload::Script(hash_a), StakePayload::Script(hash_b)) => hash_a.cmp(hash_b),
(StakePayload::Stake(hash_a), StakePayload::Stake(hash_b)) => hash_a.cmp(hash_b),
}
} else {
unreachable!("invalid reward address in withdrawals.");
}
}
#[cfg(test)]
mod tests {
use crate::{
@@ -1479,4 +1517,62 @@ mod tests {
// from the Haskell ledger / cardano node.
insta::assert_debug_snapshot!(script_context.to_plutus_data());
}
#[test]
fn script_context_withdraw() {
let redeemer = Redeemer {
tag: RedeemerTag::Reward,
index: 0,
data: Data::constr(0, vec![]),
ex_units: ExUnits {
mem: 1000000,
steps: 100000000,
},
};
// NOTE: The transaction also contains treasury donation and current treasury amount
let script_context = fixture_tx_info(
"84a7008182582000000000000000000000000000000000000000000000000000\
00000000000000000183a2005839200000000000000000000000000000000000\
0000000000000000000000111111111111111111111111111111111111111111\
11111111111111011a000f4240a2005823400000000000000000000000000000\
00000000000000000000000000008198bd431b03011a000f4240a20058235011\
1111111111111111111111111111111111111111111111111111118198bd431b\
03011a000f424002182a031a00448e0105a1581df004036eecadc2f19e95f831\
b4bc08919cde1d1088d74602bd3dcd78a2000e81581c00000000000000000000\
0000000000000000000000000000000000001601a10582840000d87a81d87980\
821a000f42401a05f5e100840300d87980821a000f42401a05f5e100f5f6",
"8182582000000000000000000000000000000000000000000000000000000000\
0000000000",
"81a40058393004036eecadc2f19e95f831b4bc08919cde1d1088d74602bd3dcd\
78a204036eecadc2f19e95f831b4bc08919cde1d1088d74602bd3dcd78a2011a\
000f4240028201d81843d8798003d818590221820359021c5902190101003232\
323232323232322232533333300c00215323330073001300937540062a660109\
211c52756e6e696e672032206172672076616c696461746f72206d696e740013\
533333300d004153330073001300937540082a66601660146ea8010494ccc021\
288a4c2a660129211856616c696461746f722072657475726e65642066616c73\
65001365600600600600600600600315330084911d52756e6e696e6720332061\
72672076616c696461746f72207370656e640013533333300d00415333007300\
1300937540082a66601660146ea8010494cccccc03800454ccc020c008c028dd\
50008a99980618059baa0011253330094a22930a998052491856616c69646174\
6f722072657475726e65642066616c7365001365600600600600600600600600\
6006006006006300c300a37540066e1d20001533007001161533007001161533\
00700116153300700116490191496e636f72726563742072656465656d657220\
7479706520666f722076616c696461746f72207370656e642e0a202020202020\
2020202020202020202020202020446f75626c6520636865636b20796f752068\
6176652077726170706564207468652072656465656d65722074797065206173\
2073706563696669656420696e20796f757220706c757475732e6a736f6e0015\
330034910b5f746d70313a20566f6964001615330024910b5f746d70303a2056\
6f696400165734ae7155ceaab9e5573eae855d21",
)
.into_script_context(&redeemer, None)
.unwrap();
// NOTE: The initial snapshot has been generated using the Haskell
// implementation of the ledger library for that same serialized
// transactions. It is meant to control that our construction of the
// script context and its serialization matches exactly those
// from the Haskell ledger / cardano node.
insta::assert_debug_snapshot!(script_context.to_plutus_data());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -736,127 +736,34 @@ impl ToPlutusData for PlutusData {
impl ToPlutusData for TimeRange {
fn to_plutus_data(&self) -> PlutusData {
match &self {
TimeRange {
lower_bound: Some(lower_bound),
upper_bound: None,
} => {
wrap_multiple_with_constr(
fn bound(bound: Option<u64>, is_lower: bool) -> PlutusData {
match bound {
Some(x) => wrap_multiple_with_constr(
0,
vec![
// LowerBound
wrap_multiple_with_constr(
0,
vec![
// Finite
wrap_with_constr(1, lower_bound.to_plutus_data()),
// Closure
true.to_plutus_data(),
],
), //UpperBound
wrap_multiple_with_constr(
0,
vec![
// PosInf
empty_constr(2),
// Closure
true.to_plutus_data(),
],
),
wrap_with_constr(1, x.to_plutus_data()),
// NOTE: Finite lower bounds are always inclusive, unlike upper bounds.
is_lower.to_plutus_data(),
],
)
}
TimeRange {
lower_bound: None,
upper_bound: Some(upper_bound),
} => {
wrap_multiple_with_constr(
),
None => wrap_multiple_with_constr(
0,
vec![
// LowerBound
wrap_multiple_with_constr(
0,
vec![
// NegInf
empty_constr(0),
// Closure
true.to_plutus_data(),
],
),
//UpperBound
wrap_multiple_with_constr(
0,
vec![
// Finite
wrap_with_constr(1, upper_bound.to_plutus_data()),
// Closure
true.to_plutus_data(),
],
),
empty_constr(if is_lower { 0 } else { 2 }),
// NOTE: Infinite bounds are always exclusive, by convention.
true.to_plutus_data(),
],
)
}
TimeRange {
lower_bound: Some(lower_bound),
upper_bound: Some(upper_bound),
} => {
wrap_multiple_with_constr(
0,
vec![
// LowerBound
wrap_multiple_with_constr(
0,
vec![
// Finite
wrap_with_constr(1, lower_bound.to_plutus_data()),
// Closure
true.to_plutus_data(),
],
),
//UpperBound
wrap_multiple_with_constr(
0,
vec![
// Finite
wrap_with_constr(1, upper_bound.to_plutus_data()),
// Closure
false.to_plutus_data(),
],
),
],
)
}
TimeRange {
lower_bound: None,
upper_bound: None,
} => {
wrap_multiple_with_constr(
0,
vec![
// LowerBound
wrap_multiple_with_constr(
0,
vec![
// NegInf
empty_constr(0),
// Closure
true.to_plutus_data(),
],
),
//UpperBound
wrap_multiple_with_constr(
0,
vec![
// PosInf
empty_constr(2),
// Closure
true.to_plutus_data(),
],
),
],
)
),
}
}
wrap_multiple_with_constr(
0,
vec![
bound(self.lower_bound, true),
bound(self.upper_bound, false),
],
)
}
}