Add script context translations for voting purpose.

This commit is contained in:
KtorZ
2024-08-13 16:18:52 +02:00
parent bfc93bf076
commit 7501538053
10 changed files with 1729 additions and 29 deletions

View File

@@ -1,13 +1,13 @@
use super::{
error::Error,
script_context::{DataLookupTable, ResolvedInput, ScriptPurpose, ScriptVersion},
script_context::{sort_voters, DataLookupTable, ResolvedInput, ScriptPurpose, ScriptVersion},
};
use itertools::Itertools;
use pallas_addresses::{Address, ScriptHash, ShelleyPaymentPart, StakePayload};
use pallas_codec::utils::Nullable;
use pallas_primitives::conway::{
Certificate, GovAction, MintedTx, PolicyId, RedeemerTag, RedeemersKey, RewardAccount,
StakeCredential, TransactionOutput,
StakeCredential, TransactionOutput, Voter,
};
use std::collections::HashMap;
@@ -166,14 +166,29 @@ pub fn scripts_needed(tx: &MintedTx, utxos: &[ResolvedInput]) -> Result<ScriptsN
})
.unwrap_or_default();
// TODO
assert!(txb.voting_procedures.is_none());
let mut voting = txb
.voting_procedures
.as_deref()
.map(|m| {
m.iter()
.filter_map(|(voter, _)| match voter {
Voter::ConstitutionalCommitteeScript(hash) | Voter::DRepScript(hash) => {
Some((ScriptPurpose::Voting(voter.clone()), *hash))
}
Voter::ConstitutionalCommitteeKey(_)
| Voter::DRepKey(_)
| Voter::StakePoolKey(_) => None,
})
.collect::<ScriptsNeeded>()
})
.unwrap_or_default();
needed.append(&mut spend);
needed.append(&mut reward);
needed.append(&mut cert);
needed.append(&mut mint);
needed.append(&mut propose);
needed.append(&mut voting);
Ok(needed)
}
@@ -329,6 +344,24 @@ fn build_redeemer_key(
Ok(redeemer_key)
}
ScriptPurpose::Voting(v) => {
let redeemer_key = tx_body
.voting_procedures
.as_deref()
.map(|m| {
m.iter()
.sorted_by(|(a, _), (b, _)| sort_voters(a, b))
.position(|x| &x.0 == v)
})
.unwrap_or_default()
.map(|index| RedeemersKey {
tag: RedeemerTag::Vote,
index: index as u32,
});
Ok(redeemer_key)
}
ScriptPurpose::Proposing(_, procedure) => {
let redeemer_key = tx_body
.proposal_procedures

View File

@@ -8,12 +8,12 @@ use pallas_crypto::hash::Hash;
use pallas_primitives::{
alonzo,
conway::{
AddrKeyhash, Certificate, Coin, DatumHash, DatumOption, GovAction, Mint,
AddrKeyhash, Certificate, Coin, DatumHash, DatumOption, GovAction, GovActionId, Mint,
MintedTransactionBody, MintedTransactionOutput, MintedTx, MintedWitnessSet, NativeScript,
PlutusData, PlutusV1Script, PlutusV2Script, PlutusV3Script, PolicyId,
PostAlonzoTransactionOutput, ProposalProcedure, PseudoDatumOption, PseudoScript, Redeemer,
RedeemerTag, RedeemersKey, RequiredSigners, RewardAccount, ScriptHash, StakeCredential,
TransactionInput, TransactionOutput, Value,
TransactionInput, TransactionOutput, Value, Voter, VotingProcedure,
},
};
use pallas_traverse::{ComputeHash, OriginalHash};
@@ -53,6 +53,7 @@ pub enum ScriptInfo<T> {
Spending(TransactionInput, T),
Rewarding(StakeCredential),
Certifying(usize, Certificate),
Voting(Voter),
Proposing(usize, ProposalProcedure),
}
@@ -67,6 +68,7 @@ impl ScriptPurpose {
}
Self::Rewarding(stake_credential) => ScriptInfo::Rewarding(stake_credential),
Self::Certifying(ix, certificate) => ScriptInfo::Certifying(ix, certificate),
Self::Voting(voter) => ScriptInfo::Voting(voter),
Self::Proposing(ix, procedure) => ScriptInfo::Proposing(ix, procedure),
}
}
@@ -220,7 +222,7 @@ impl TxInfoV1 {
let redeemers = get_redeemers_info(
&tx.transaction_witness_set,
script_purpose_builder(&inputs[..], &mint, &certificates, &withdrawals, &[]),
script_purpose_builder(&inputs[..], &mint, &certificates, &withdrawals, &[], &[]),
)?;
Ok(TxInfo::V1(TxInfoV1 {
@@ -269,7 +271,7 @@ impl TxInfoV2 {
let redeemers = get_redeemers_info(
&tx.transaction_witness_set,
script_purpose_builder(&inputs[..], &mint, &certificates, &withdrawals, &[]),
script_purpose_builder(&inputs[..], &mint, &certificates, &withdrawals, &[], &[]),
)?;
let reference_inputs = tx
@@ -310,12 +312,11 @@ pub struct TxInfoV3 {
pub signatories: Vec<AddrKeyhash>,
pub redeemers: KeyValuePairs<ScriptPurpose, Redeemer>,
pub data: KeyValuePairs<DatumHash, PlutusData>,
pub proposal_procedures: Vec<ProposalProcedure>,
pub id: Hash<32>,
pub votes: KeyValuePairs<Voter, KeyValuePairs<GovActionId, VotingProcedure>>,
pub proposal_procedures: Vec<ProposalProcedure>,
pub current_treasury_amount: Option<Coin>,
pub treasury_donation: Option<PositiveCoin>,
// TODO:
// votes : KeyValuePairs<Voter, KeyValuePairs<GovernanceActionId, Vote>>
}
impl TxInfoV3 {
@@ -336,6 +337,8 @@ impl TxInfoV3 {
let proposal_procedures =
get_proposal_procedures_info(&tx.transaction_body.proposal_procedures);
let votes = get_votes_info(&tx.transaction_body.voting_procedures);
let redeemers = get_redeemers_info(
&tx.transaction_witness_set,
script_purpose_builder(
@@ -344,6 +347,7 @@ impl TxInfoV3 {
&certificates,
&withdrawals,
&proposal_procedures,
&votes.iter().map(|(k, _v)| k).collect_vec()[..],
),
)?;
@@ -368,6 +372,7 @@ impl TxInfoV3 {
data: KeyValuePairs::from(get_data_info(&tx.transaction_witness_set)),
redeemers,
proposal_procedures,
votes,
current_treasury_amount: get_current_treasury_amount_info(
&tx.transaction_body.treasury_value,
),
@@ -713,12 +718,44 @@ pub fn get_redeemers_info<'a>(
))
}
pub fn get_votes_info(
votes: &Option<
NonEmptyKeyValuePairs<Voter, NonEmptyKeyValuePairs<GovActionId, VotingProcedure>>,
>,
) -> KeyValuePairs<Voter, KeyValuePairs<GovActionId, VotingProcedure>> {
KeyValuePairs::from(
votes
.as_deref()
.map(|votes| {
votes
.iter()
.sorted_by(|(a, _), (b, _)| sort_voters(a, b))
.cloned()
.map(|(voter, actions)| {
(
voter,
KeyValuePairs::from(
actions
.iter()
.sorted_by(|(a, _), (b, _)| sort_gov_action_id(a, b))
.cloned()
.collect::<Vec<_>>(),
),
)
})
.collect_vec()
})
.unwrap_or_default(),
)
}
fn script_purpose_builder<'a>(
inputs: &'a [TxInInfo],
mint: &'a MintValue,
certificates: &'a [Certificate],
withdrawals: &'a KeyValuePairs<Address, Coin>,
proposal_procedures: &'a [ProposalProcedure],
votes: &'a [&'a Voter],
) -> impl Fn(&'a RedeemersKey) -> Result<ScriptPurpose, Error> {
move |redeemer: &'a RedeemersKey| {
let tag = redeemer.tag;
@@ -754,12 +791,16 @@ fn script_purpose_builder<'a>(
})
.transpose()?,
RedeemerTag::Vote => votes
.get(index)
.cloned()
.cloned()
.map(ScriptPurpose::Voting),
RedeemerTag::Propose => proposal_procedures
.get(redeemer.index as usize)
.get(index)
.cloned()
.map(|p| ScriptPurpose::Proposing(index, p)),
tag => todo!("get_script_purpose for {tag:?}"),
}
.ok_or(Error::ExtraneousRedeemer)
}
@@ -867,6 +908,18 @@ pub fn find_script(
_ => Err(Error::NonScriptStakeCredential),
}),
RedeemerTag::Vote => get_votes_info(&tx.transaction_body.voting_procedures)
.get(redeemer.index as usize)
.ok_or(Error::MissingScriptForRedeemer)
.and_then(|(voter, _)| match voter {
Voter::ConstitutionalCommitteeScript(hash) => Ok(hash),
Voter::ConstitutionalCommitteeKey(..) => Err(Error::NonScriptStakeCredential),
Voter::DRepScript(hash) => Ok(hash),
Voter::DRepKey(..) => Err(Error::NonScriptStakeCredential),
Voter::StakePoolKey(..) => Err(Error::NonScriptStakeCredential),
})
.and_then(lookup_script),
RedeemerTag::Propose => {
get_proposal_procedures_info(&tx.transaction_body.proposal_procedures)
.get(redeemer.index as usize)
@@ -884,8 +937,6 @@ pub fn find_script(
})
.and_then(lookup_script)
}
RedeemerTag::Vote => todo!("find_script: RedeemerTag::Vote"),
}
}
@@ -1006,6 +1057,35 @@ fn sort_redeemers(a: &RedeemersKey, b: &RedeemersKey) -> Ordering {
}
}
pub fn sort_voters(a: &Voter, b: &Voter) -> Ordering {
fn explode(voter: &Voter) -> (usize, &Hash<28>) {
match voter {
Voter::ConstitutionalCommitteeScript(hash) => (0, hash),
Voter::ConstitutionalCommitteeKey(hash) => (1, hash),
Voter::DRepScript(hash) => (2, hash),
Voter::DRepKey(hash) => (3, hash),
Voter::StakePoolKey(hash) => (4, hash),
}
}
let (tag_a, hash_a) = explode(a);
let (tag_b, hash_b) = explode(b);
if tag_a == tag_b {
hash_a.cmp(hash_b)
} else {
tag_a.cmp(&tag_b)
}
}
fn sort_gov_action_id(a: &GovActionId, b: &GovActionId) -> Ordering {
if a.transaction_id == b.transaction_id {
a.action_index.cmp(&b.action_index)
} else {
a.transaction_id.cmp(&b.transaction_id)
}
}
#[cfg(test)]
mod tests {
use crate::{
@@ -1346,4 +1426,57 @@ mod tests {
// from the Haskell ledger / cardano node.
insta::assert_debug_snapshot!(script_context.to_plutus_data());
}
#[test]
fn script_context_voting() {
let redeemer = Redeemer {
tag: RedeemerTag::Vote,
index: 0,
data: Data::constr(0, vec![Data::integer(42.into())]),
ex_units: ExUnits {
mem: 1000000,
steps: 100000000,
},
};
// NOTE: The transaction also contains treasury donation and current treasury amount
let script_context = fixture_tx_info(
"84a4008182582000000000000000000000000000000000000000000000000000\
0000000000000000018002182a13a58200581c00000000000000000000000000\
000000000000000000000000000000a182582099999999999999999999999999\
9999999999999999999999999999999999999918988200827668747470733a2f\
2f61696b656e2d6c616e672e6f72675820000000000000000000000000000000\
00000000000000000000000000000000008202581c0000000000000000000000\
0000000000000000000000000000000000a38258209999999999999999999999\
999999999999999999999999999999999999999999008202f682582088888888\
88888888888888888888888888888888888888888888888888888888018202f6\
8258207777777777777777777777777777777777777777777777777777777777\
777777028202f68203581c43fa47afc68a7913fbe2f400e3849cb492d9a2610c\
85966de0f2ba1ea1825820999999999999999999999999999999999999999999\
9999999999999999999999038200f68204581c00000000000000000000000000\
000000000000000000000000000000a182582099999999999999999999999999\
99999999999999999999999999999999999999048201f68201581c43fa47afc6\
8a7913fbe2f400e3849cb492d9a2610c85966de0f2ba1ea18258209999999999\
999999999999999999999999999999999999999999999999999999018201f6a2\
0582840402d87980821a000f42401a05f5e100840400d87981182a821a000f42\
401a05f5e1000781587d587b0101003232323232323225333333008001153330\
033370e900018029baa001153330073006375400224a66600894452615330054\
911856616c696461746f722072657475726e65642066616c7365001365600200\
2002002002002153300249010b5f746d70303a20566f696400165734ae7155ce\
aab9e5573eae91f5f6",
"8182582000000000000000000000000000000000000000000000000000000000\
0000000000",
"81a200581d600000000000000000000000000000000000000000000000000000\
0000011a000f4240",
)
.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());
}
}

View File

@@ -18,7 +18,8 @@ use pallas_primitives::conway::{
AssetName, BigInt, Certificate, Coin, Constitution, Constr, DRep, DRepVotingThresholds,
DatumOption, ExUnitPrices, ExUnits, GovAction, GovActionId, Mint, PlutusData, PolicyId,
PoolVotingThresholds, ProposalProcedure, ProtocolParamUpdate, PseudoScript, RationalNumber,
Redeemer, ScriptRef, StakeCredential, TransactionInput, TransactionOutput, Value,
Redeemer, ScriptRef, StakeCredential, TransactionInput, TransactionOutput, Value, Vote, Voter,
VotingProcedure,
};
use pallas_traverse::ComputeHash;
@@ -953,6 +954,9 @@ impl ToPlutusData for ScriptPurpose {
ScriptPurpose::Certifying(ix, dcert) => {
wrap_multiple_with_constr(3, vec![ix.to_plutus_data(), dcert.to_plutus_data()])
}
ScriptPurpose::Voting(voter) => {
wrap_multiple_with_constr(4, vec![voter.to_plutus_data()])
}
ScriptPurpose::Proposing(ix, procedure) => {
wrap_multiple_with_constr(5, vec![ix.to_plutus_data(), procedure.to_plutus_data()])
}
@@ -1293,6 +1297,42 @@ impl<'a> ToPlutusData for WithWrappedStakeCredential<'a, KeyValuePairs<Address,
}
}
impl ToPlutusData for Voter {
fn to_plutus_data(&self) -> PlutusData {
match self {
Voter::ConstitutionalCommitteeScript(hash) => {
wrap_with_constr(0, StakeCredential::Scripthash(*hash).to_plutus_data())
}
Voter::ConstitutionalCommitteeKey(hash) => {
wrap_with_constr(0, StakeCredential::AddrKeyhash(*hash).to_plutus_data())
}
Voter::DRepScript(hash) => {
wrap_with_constr(1, StakeCredential::Scripthash(*hash).to_plutus_data())
}
Voter::DRepKey(hash) => {
wrap_with_constr(1, StakeCredential::AddrKeyhash(*hash).to_plutus_data())
}
Voter::StakePoolKey(hash) => wrap_with_constr(2, hash.to_plutus_data()),
}
}
}
impl ToPlutusData for VotingProcedure {
fn to_plutus_data(&self) -> PlutusData {
self.vote.to_plutus_data()
}
}
impl ToPlutusData for Vote {
fn to_plutus_data(&self) -> PlutusData {
match self {
Vote::No => empty_constr(0),
Vote::Yes => empty_constr(1),
Vote::Abstain => empty_constr(2),
}
}
}
impl<T> ToPlutusData for ScriptInfo<T>
where
T: ToPlutusData,
@@ -1309,6 +1349,7 @@ where
ScriptInfo::Certifying(ix, dcert) => {
wrap_multiple_with_constr(3, vec![ix.to_plutus_data(), dcert.to_plutus_data()])
}
ScriptInfo::Voting(voter) => wrap_multiple_with_constr(4, vec![voter.to_plutus_data()]),
ScriptInfo::Proposing(ix, procedure) => {
wrap_multiple_with_constr(5, vec![ix.to_plutus_data(), procedure.to_plutus_data()])
}
@@ -1370,7 +1411,7 @@ impl ToPlutusData for TxInfo {
tx_info.redeemers.to_plutus_data(),
tx_info.data.to_plutus_data(),
tx_info.id.to_plutus_data(),
Data::map(vec![]), // TODO tx_info.votes :: Map Voter (Map GovernanceActionId Vote)
tx_info.votes.to_plutus_data(),
tx_info.proposal_procedures.to_plutus_data(),
tx_info.current_treasury_amount.to_plutus_data(),
tx_info.treasury_donation.to_plutus_data(),