Add (partial) support for simulating contract with proposal procedures

This covers every proposal procedures but protocol parameters, this
  one is yet to be done. It spans over 30+ fields, and felt like it is a
  big enough piece to tackle it on its own.
This commit is contained in:
KtorZ 2024-08-10 23:24:30 +02:00
parent 6b6bace8a5
commit cfca0da4e9
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
6 changed files with 2292 additions and 90 deletions

10
Cargo.lock generated vendored
View File

@ -1942,7 +1942,7 @@ checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f"
[[package]]
name = "pallas-addresses"
version = "0.29.0"
source = "git+https://github.com/KtorZ/pallas.git?rev=8ea5a1adc9919b70b213dfe597e920d6e113120c#8ea5a1adc9919b70b213dfe597e920d6e113120c"
source = "git+https://github.com/KtorZ/pallas.git?rev=0cdd6393bb3fbceb3b5209ad248877b381822e90#0cdd6393bb3fbceb3b5209ad248877b381822e90"
dependencies = [
"base58",
"bech32",
@ -1957,7 +1957,7 @@ dependencies = [
[[package]]
name = "pallas-codec"
version = "0.29.0"
source = "git+https://github.com/KtorZ/pallas.git?rev=8ea5a1adc9919b70b213dfe597e920d6e113120c#8ea5a1adc9919b70b213dfe597e920d6e113120c"
source = "git+https://github.com/KtorZ/pallas.git?rev=0cdd6393bb3fbceb3b5209ad248877b381822e90#0cdd6393bb3fbceb3b5209ad248877b381822e90"
dependencies = [
"hex",
"minicbor",
@ -1969,7 +1969,7 @@ dependencies = [
[[package]]
name = "pallas-crypto"
version = "0.29.0"
source = "git+https://github.com/KtorZ/pallas.git?rev=8ea5a1adc9919b70b213dfe597e920d6e113120c#8ea5a1adc9919b70b213dfe597e920d6e113120c"
source = "git+https://github.com/KtorZ/pallas.git?rev=0cdd6393bb3fbceb3b5209ad248877b381822e90#0cdd6393bb3fbceb3b5209ad248877b381822e90"
dependencies = [
"cryptoxide",
"hex",
@ -1982,7 +1982,7 @@ dependencies = [
[[package]]
name = "pallas-primitives"
version = "0.29.0"
source = "git+https://github.com/KtorZ/pallas.git?rev=8ea5a1adc9919b70b213dfe597e920d6e113120c#8ea5a1adc9919b70b213dfe597e920d6e113120c"
source = "git+https://github.com/KtorZ/pallas.git?rev=0cdd6393bb3fbceb3b5209ad248877b381822e90#0cdd6393bb3fbceb3b5209ad248877b381822e90"
dependencies = [
"base58",
"bech32",
@ -1997,7 +1997,7 @@ dependencies = [
[[package]]
name = "pallas-traverse"
version = "0.29.0"
source = "git+https://github.com/KtorZ/pallas.git?rev=8ea5a1adc9919b70b213dfe597e920d6e113120c#8ea5a1adc9919b70b213dfe597e920d6e113120c"
source = "git+https://github.com/KtorZ/pallas.git?rev=0cdd6393bb3fbceb3b5209ad248877b381822e90#0cdd6393bb3fbceb3b5209ad248877b381822e90"
dependencies = [
"hex",
"itertools 0.13.0",

View File

@ -49,11 +49,11 @@ x86_64-unknown-linux-gnu = "ubuntu-22.04"
walkdir = "2.3.2"
insta = { version = "1.30.0", features = ["yaml", "json", "redactions"] }
miette = { version = "7.2.0", features = ["fancy"] }
pallas-addresses = { git = "https://github.com/KtorZ/pallas.git", rev = "8ea5a1adc9919b70b213dfe597e920d6e113120c" }
pallas-codec = { git = "https://github.com/KtorZ/pallas.git", rev = "8ea5a1adc9919b70b213dfe597e920d6e113120c", features = ["num-bigint"] }
pallas-crypto = { git = "https://github.com/KtorZ/pallas.git", rev = "8ea5a1adc9919b70b213dfe597e920d6e113120c" }
pallas-primitives = { git = "https://github.com/KtorZ/pallas.git", rev = "8ea5a1adc9919b70b213dfe597e920d6e113120c" }
pallas-traverse = { git = "https://github.com/KtorZ/pallas.git", rev = "8ea5a1adc9919b70b213dfe597e920d6e113120c" }
pallas-addresses = { git = "https://github.com/KtorZ/pallas.git", rev = "0cdd6393bb3fbceb3b5209ad248877b381822e90" }
pallas-codec = { git = "https://github.com/KtorZ/pallas.git", rev = "0cdd6393bb3fbceb3b5209ad248877b381822e90", features = ["num-bigint"] }
pallas-crypto = { git = "https://github.com/KtorZ/pallas.git", rev = "0cdd6393bb3fbceb3b5209ad248877b381822e90" }
pallas-primitives = { git = "https://github.com/KtorZ/pallas.git", rev = "0cdd6393bb3fbceb3b5209ad248877b381822e90" }
pallas-traverse = { git = "https://github.com/KtorZ/pallas.git", rev = "0cdd6393bb3fbceb3b5209ad248877b381822e90" }
[profile.dev.package.insta]
opt-level = 3

View File

@ -67,6 +67,8 @@ pub enum Error {
NonScriptWithdrawal,
#[error("stake credential points to a non-script withdrawal")]
NonScriptStakeCredential,
#[error("the designated procedure defines no guardrail script")]
NoGuardrailScriptForProcedure,
#[error("cost model not found for language\n{:>13} {:?}", "Language", .0)]
CostModelNotFound(Language),
#[error("unsupported era, please use Conway\n{:>13} {0}", "Decoder error")]

View File

@ -4,9 +4,10 @@ use super::{
};
use itertools::Itertools;
use pallas_addresses::{Address, ScriptHash, ShelleyPaymentPart, StakePayload};
use pallas_codec::utils::Nullable;
use pallas_primitives::conway::{
Certificate, MintedTx, PolicyId, RedeemerTag, RedeemersKey, RewardAccount, StakeCredential,
TransactionOutput,
Certificate, GovAction, MintedTx, PolicyId, RedeemerTag, RedeemersKey, RewardAccount,
StakeCredential, TransactionOutput,
};
use std::collections::HashMap;
@ -106,14 +107,15 @@ pub fn scripts_needed(tx: &MintedTx, utxos: &[ResolvedInput]) -> Result<ScriptsN
.as_deref()
.map(|m| {
m.iter()
.filter_map(|cert| {
.enumerate()
.filter_map(|(ix, cert)| {
// only Dereg and Deleg certs can require scripts
match cert {
Certificate::StakeDeregistration(StakeCredential::Scripthash(h)) => {
Some((ScriptPurpose::Certifying(cert.clone()), *h))
Some((ScriptPurpose::Certifying(ix, cert.clone()), *h))
}
Certificate::StakeDelegation(StakeCredential::Scripthash(h), _) => {
Some((ScriptPurpose::Certifying(cert.clone()), *h))
Some((ScriptPurpose::Certifying(ix, cert.clone()), *h))
}
_ => None,
}
@ -132,14 +134,39 @@ pub fn scripts_needed(tx: &MintedTx, utxos: &[ResolvedInput]) -> Result<ScriptsN
})
.unwrap_or_default();
let mut propose = txb
.proposal_procedures
.as_deref()
.map(|m| {
m.iter()
.enumerate()
.filter_map(|(ix, procedure)| match procedure.gov_action {
GovAction::ParameterChange(_, _, Nullable::Some(hash)) => {
Some((ScriptPurpose::Proposing(ix, procedure.clone()), hash))
}
GovAction::TreasuryWithdrawals(_, Nullable::Some(hash)) => {
Some((ScriptPurpose::Proposing(ix, procedure.clone()), hash))
}
GovAction::HardForkInitiation(..)
| GovAction::Information
| GovAction::NewConstitution(..)
| GovAction::TreasuryWithdrawals(..)
| GovAction::ParameterChange(..)
| GovAction::NoConfidence(..)
| GovAction::UpdateCommittee(..) => None,
})
.collect::<ScriptsNeeded>()
})
.unwrap_or_default();
// TODO
assert!(txb.proposal_procedures.is_none());
assert!(txb.voting_procedures.is_none());
needed.append(&mut spend);
needed.append(&mut reward);
needed.append(&mut cert);
needed.append(&mut mint);
needed.append(&mut propose);
Ok(needed)
}
@ -191,7 +218,7 @@ pub fn has_exact_set_of_redeemers(
let extra: Vec<_> = wits_redeemer_keys
.into_iter()
.filter(|x| !needed_redeemer_keys.contains(x))
.map(|x| format!("{x:?}"))
.map(|x| format!("{:?}[{:?}]", x.tag, x.index))
.collect();
if !missing.is_empty() || !extra.is_empty() {
@ -281,7 +308,7 @@ fn build_redeemer_key(
Ok(redeemer_key)
}
ScriptPurpose::Certifying(d) => {
ScriptPurpose::Certifying(_, d) => {
let redeemer_key = tx_body
.certificates
.as_deref()
@ -294,5 +321,19 @@ fn build_redeemer_key(
Ok(redeemer_key)
}
ScriptPurpose::Proposing(_, procedure) => {
let redeemer_key = tx_body
.proposal_procedures
.as_deref()
.map(|m| m.iter().position(|x| x == procedure))
.unwrap_or_default()
.map(|index| RedeemersKey {
tag: RedeemerTag::Propose,
index: index as u32,
});
Ok(redeemer_key)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -6,12 +6,16 @@ use crate::{
machine::runtime::{convert_constr_to_tag, ANY_TAG},
tx::script_context::from_alonzo_output,
};
use pallas_addresses::{Address, ShelleyDelegationPart, ShelleyPaymentPart, StakePayload};
use pallas_codec::utils::{AnyUInt, Bytes, Int, KeyValuePairs, NonEmptyKeyValuePairs};
use pallas_addresses::{
Address, ShelleyDelegationPart, ShelleyPaymentPart, StakeAddress, StakePayload,
};
use pallas_codec::utils::{AnyUInt, Bytes, Int, KeyValuePairs, NonEmptyKeyValuePairs, Nullable};
use pallas_crypto::hash::Hash;
use pallas_primitives::conway::{
AssetName, BigInt, Certificate, Coin, Constr, DatumOption, Mint, PlutusData, PolicyId,
PseudoScript, Redeemer, ScriptRef, StakeCredential, TransactionInput, TransactionOutput, Value,
AssetName, BigInt, Certificate, Coin, Constitution, Constr, DatumOption, GovAction,
GovActionId, Mint, PlutusData, PolicyId, ProposalProcedure, ProtocolParamUpdate, PseudoScript,
RationalNumber, Redeemer, ScriptRef, StakeCredential, TransactionInput, TransactionOutput,
Value,
};
use pallas_traverse::ComputeHash;
@ -34,6 +38,8 @@ fn empty_constr(index: u64) -> PlutusData {
struct WithWrappedTransactionId<'a, T>(&'a T);
struct WithWrappedStakeCredential<'a, T>(&'a T);
struct WithZeroAdaAsset<'a, T>(&'a T);
struct WithOptionDatum<'a, T>(&'a T);
@ -47,6 +53,23 @@ pub struct MintValue {
pub mint_value: Mint,
}
impl ToPlutusData for bool {
fn to_plutus_data(&self) -> PlutusData {
match self {
false => empty_constr(0),
true => empty_constr(1),
}
}
}
impl ToPlutusData for StakeAddress {
fn to_plutus_data(&self) -> PlutusData {
match self.payload() {
StakePayload::Stake(x) => wrap_with_constr(0, x.to_plutus_data()),
StakePayload::Script(x) => wrap_with_constr(1, x.to_plutus_data()),
}
}
}
impl ToPlutusData for Address {
fn to_plutus_data(&self) -> PlutusData {
match self {
@ -64,12 +87,16 @@ impl ToPlutusData for Address {
};
let stake_part_plutus_data = match stake_part {
ShelleyDelegationPart::Key(stake_keyhash) => {
Some(StakeCredential::AddrKeyhash(*stake_keyhash)).to_plutus_data()
}
ShelleyDelegationPart::Script(script_hash) => {
Some(StakeCredential::Scripthash(*script_hash)).to_plutus_data()
}
ShelleyDelegationPart::Key(stake_keyhash) => Some(wrap_with_constr(
0,
StakeCredential::AddrKeyhash(*stake_keyhash).to_plutus_data(),
))
.to_plutus_data(),
ShelleyDelegationPart::Script(script_hash) => Some(wrap_with_constr(
0,
StakeCredential::Scripthash(*script_hash).to_plutus_data(),
))
.to_plutus_data(),
ShelleyDelegationPart::Pointer(pointer) => Some(wrap_multiple_with_constr(
1,
vec![
@ -84,17 +111,7 @@ impl ToPlutusData for Address {
wrap_multiple_with_constr(0, vec![payment_part_plutus_data, stake_part_plutus_data])
}
Address::Stake(stake_address) => {
// This is right now only used in Withdrawals (Reward account)
match stake_address.payload() {
StakePayload::Stake(stake_keyhash) => {
StakeCredential::AddrKeyhash(*stake_keyhash).to_plutus_data()
}
StakePayload::Script(script_hash) => {
StakeCredential::Scripthash(*script_hash).to_plutus_data()
}
}
}
Address::Stake(stake_address) => stake_address.to_plutus_data(),
_ => unreachable!(),
}
}
@ -230,12 +247,24 @@ impl ToPlutusData for i64 {
}
}
impl ToPlutusData for u32 {
fn to_plutus_data(&self) -> PlutusData {
PlutusData::BigInt(BigInt::Int(Int::try_from(*self as i128).unwrap()))
}
}
impl ToPlutusData for u64 {
fn to_plutus_data(&self) -> PlutusData {
PlutusData::BigInt(BigInt::Int(Int::try_from(*self as i128).unwrap()))
}
}
impl ToPlutusData for usize {
fn to_plutus_data(&self) -> PlutusData {
PlutusData::BigInt(BigInt::Int(Int::try_from(*self as i128).unwrap()))
}
}
impl<'a> ToPlutusData for WithZeroAdaAsset<'a, Value> {
fn to_plutus_data(&self) -> PlutusData {
match self.0 {
@ -443,16 +472,13 @@ impl ToPlutusData for TransactionOutput {
}
impl ToPlutusData for StakeCredential {
// Stake Credential needs to be wrapped inside another Constr, because we could have either a StakingHash or a StakingPtr
// The current implementation of StakeCredential doesn't capture the credential of a Pointer address.
// So a StakeCredential for a Pointer address needs to be converted separately
fn to_plutus_data(&self) -> PlutusData {
match self {
StakeCredential::AddrKeyhash(addr_keyhas) => {
wrap_with_constr(0, wrap_with_constr(0, addr_keyhas.to_plutus_data()))
wrap_with_constr(0, addr_keyhas.to_plutus_data())
}
StakeCredential::Scripthash(script_hash) => {
wrap_with_constr(0, wrap_with_constr(1, script_hash.to_plutus_data()))
wrap_with_constr(1, script_hash.to_plutus_data())
}
}
}
@ -722,6 +748,10 @@ impl<'a> ToPlutusData for WithWrappedTransactionId<'a, ScriptPurpose> {
ScriptPurpose::Spending(out_ref, ()) => {
wrap_with_constr(1, WithWrappedTransactionId(out_ref).to_plutus_data())
}
// NOTE: This is a _small_ abuse of the 'WithWrappedTransactionId'. We know the wrapped
// is needed for V1 and V2, and it also appears that for V1 and V2, the certifying
// purpose mustn't include the certificate index. So, we also short-circuit it here.
ScriptPurpose::Certifying(_, dcert) => wrap_with_constr(3, dcert.to_plutus_data()),
otherwise => otherwise.to_plutus_data(),
}
}
@ -735,11 +765,167 @@ impl ToPlutusData for ScriptPurpose {
ScriptPurpose::Rewarding(stake_credential) => {
wrap_with_constr(2, stake_credential.to_plutus_data())
}
ScriptPurpose::Certifying(dcert) => wrap_with_constr(3, dcert.to_plutus_data()),
ScriptPurpose::Certifying(ix, dcert) => {
wrap_multiple_with_constr(3, vec![ix.to_plutus_data(), dcert.to_plutus_data()])
}
ScriptPurpose::Proposing(ix, procedure) => {
wrap_multiple_with_constr(5, vec![ix.to_plutus_data(), procedure.to_plutus_data()])
}
}
}
}
impl ToPlutusData for ProposalProcedure {
fn to_plutus_data(&self) -> PlutusData {
wrap_multiple_with_constr(
0,
vec![
self.deposit.to_plutus_data(),
Address::from_bytes(&self.reward_account)
.unwrap()
.to_plutus_data(),
self.gov_action.to_plutus_data(),
],
)
}
}
impl<T> ToPlutusData for Nullable<T>
where
T: ToPlutusData + Clone,
{
fn to_plutus_data(&self) -> PlutusData {
match self {
Nullable::Some(t) => wrap_with_constr(0, t.to_plutus_data()),
Nullable::Null | Nullable::Undefined => empty_constr(1),
}
}
}
impl ToPlutusData for GovActionId {
fn to_plutus_data(&self) -> PlutusData {
wrap_multiple_with_constr(
0,
vec![
self.transaction_id.to_plutus_data(),
self.action_index.to_plutus_data(),
],
)
}
}
impl ToPlutusData for ProtocolParamUpdate {
fn to_plutus_data(&self) -> PlutusData {
todo!("ToPlutusData for ProtocolParamUpdate")
}
}
impl ToPlutusData for GovAction {
fn to_plutus_data(&self) -> PlutusData {
match self {
GovAction::ParameterChange(previous_action, params, guardrail) => {
wrap_multiple_with_constr(
0,
vec![
previous_action.to_plutus_data(),
params.as_ref().to_plutus_data(),
guardrail.to_plutus_data(),
],
)
}
GovAction::HardForkInitiation(previous_action, version) => wrap_multiple_with_constr(
1,
vec![previous_action.to_plutus_data(), version.to_plutus_data()],
),
GovAction::TreasuryWithdrawals(withdrawals, guardrail) => wrap_multiple_with_constr(
2,
vec![
KeyValuePairs::from(
withdrawals
.iter()
.map(|(reward_account, amount)| {
(
Address::from_bytes(reward_account)
.expect("Invalid stake address in treasury withdrawal?"),
*amount,
)
})
.collect::<Vec<_>>(),
)
.to_plutus_data(),
guardrail.to_plutus_data(),
],
),
GovAction::NoConfidence(previous_action) => {
wrap_with_constr(3, previous_action.to_plutus_data())
}
GovAction::UpdateCommittee(previous_action, removed, added, quorum) => {
wrap_multiple_with_constr(
4,
vec![
previous_action.to_plutus_data(),
removed.to_plutus_data(),
added.to_plutus_data(),
quorum.to_plutus_data(),
],
)
}
GovAction::NewConstitution(previous_action, constitution) => wrap_multiple_with_constr(
5,
vec![
previous_action.to_plutus_data(),
constitution.to_plutus_data(),
],
),
GovAction::Information => empty_constr(6),
}
}
}
impl ToPlutusData for Constitution {
fn to_plutus_data(&self) -> PlutusData {
wrap_with_constr(0, self.guardrail_script.to_plutus_data())
}
}
impl ToPlutusData for RationalNumber {
fn to_plutus_data(&self) -> PlutusData {
(self.numerator, self.denominator).to_plutus_data()
}
}
impl<'a> ToPlutusData for WithWrappedStakeCredential<'a, Vec<(Address, Coin)>> {
fn to_plutus_data(&self) -> PlutusData {
self.0
.iter()
.map(|(reward_account, amount)| {
(
wrap_with_constr(0, reward_account.to_plutus_data()),
*amount,
)
})
.collect::<Vec<_>>()
.to_plutus_data()
}
}
impl<'a> ToPlutusData for WithWrappedStakeCredential<'a, KeyValuePairs<Address, Coin>> {
fn to_plutus_data(&self) -> PlutusData {
KeyValuePairs::from(
self.0
.iter()
.map(|(reward_account, amount)| {
(
wrap_with_constr(0, reward_account.to_plutus_data()),
*amount,
)
})
.collect::<Vec<_>>(),
)
.to_plutus_data()
}
}
impl<T> ToPlutusData for ScriptInfo<T>
where
T: ToPlutusData,
@ -753,7 +939,12 @@ where
ScriptInfo::Rewarding(stake_credential) => {
wrap_with_constr(2, stake_credential.to_plutus_data())
}
ScriptInfo::Certifying(dcert) => wrap_with_constr(3, dcert.to_plutus_data()),
ScriptInfo::Certifying(ix, dcert) => {
wrap_multiple_with_constr(3, vec![ix.to_plutus_data(), dcert.to_plutus_data()])
}
ScriptInfo::Proposing(ix, procedure) => {
wrap_multiple_with_constr(5, vec![ix.to_plutus_data(), procedure.to_plutus_data()])
}
}
}
}
@ -772,7 +963,7 @@ impl ToPlutusData for TxInfo {
WithZeroAdaAsset(&tx_info.fee).to_plutus_data(),
WithZeroAdaAsset(&tx_info.mint).to_plutus_data(),
tx_info.certificates.to_plutus_data(),
tx_info.withdrawals.to_plutus_data(),
WithWrappedStakeCredential(&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(),
@ -789,7 +980,7 @@ impl ToPlutusData for TxInfo {
WithZeroAdaAsset(&tx_info.fee).to_plutus_data(),
WithZeroAdaAsset(&tx_info.mint).to_plutus_data(),
tx_info.certificates.to_plutus_data(),
tx_info.withdrawals.to_plutus_data(),
WithWrappedStakeCredential(&tx_info.withdrawals).to_plutus_data(),
tx_info.valid_range.to_plutus_data(),
tx_info.signatories.to_plutus_data(),
WithWrappedTransactionId(&tx_info.redeemers).to_plutus_data(),
@ -813,7 +1004,7 @@ impl ToPlutusData for TxInfo {
tx_info.data.to_plutus_data(),
tx_info.id.to_plutus_data(),
Data::map(vec![]), // TODO tx_info.votes :: Map Voter (Map GovernanceActionId Vote)
Data::list(vec![]), // TODO tx_info.proposal_procedures :: [ProposalProcedure]
tx_info.proposal_procedures.to_plutus_data(),
empty_constr(1), // TODO tx_info.current_treasury_amount :: Haskell.Maybe V2.Lovelace
empty_constr(1), // TODO tx_info.treasury_donation :: Haskell.Maybe V2.Lovelace
],
@ -847,12 +1038,3 @@ impl ToPlutusData for ScriptContext {
}
}
}
impl ToPlutusData for bool {
fn to_plutus_data(&self) -> PlutusData {
match self {
false => empty_constr(0),
true => empty_constr(1),
}
}
}