From b4189230c92033d1aff14f77ef688a7ea2f984ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Fri, 7 Mar 2025 16:12:28 +0300 Subject: [PATCH 01/13] Fix Credential decoding --- .../src/miniprotocols/localtxsubmission/primitives.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs b/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs index ac2fdff40..104e544cc 100644 --- a/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs +++ b/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs @@ -20,9 +20,9 @@ pub type Mint = Multiasset; #[derive(Debug, Decode, Encode, Clone, Hash, PartialEq, Eq)] #[cbor(flat)] pub enum Credential { - #[n(0)] - ScriptHashObj(#[n(0)] ScriptHash), #[n(1)] + ScriptHashObj(#[n(0)] ScriptHash), + #[n(0)] KeyHashObj(#[n(0)] AddrKeyhash), } From 1cf2b8751b115197fa4846e875d8d670bda00ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Tue, 11 Mar 2025 13:38:31 +0300 Subject: [PATCH 02/13] Match ProposalProcedure ordering with Haskell implementation --- pallas-hardano/src/display/haskell_display.rs | 61 +++++-------------- .../localstate/queries_v16/codec.rs | 32 ++++++++++ .../localstate/queries_v16/mod.rs | 61 +++++++++++++------ .../localtxsubmission/primitives.rs | 4 +- .../localtxsubmission/protocol.rs | 37 +++++------ 5 files changed, 114 insertions(+), 81 deletions(-) diff --git a/pallas-hardano/src/display/haskell_display.rs b/pallas-hardano/src/display/haskell_display.rs index f391bb63c..8a6eb6ce4 100644 --- a/pallas-hardano/src/display/haskell_display.rs +++ b/pallas-hardano/src/display/haskell_display.rs @@ -20,10 +20,10 @@ use pallas_network::miniprotocols::{ handshake::NetworkMagic, localstate::queries_v16::{ primitives::Bytes, Anchor, AssetName, BoundedBytes, Constitution, CostModel, CostModels, - DRep, DRepVotingThresholds, DatumHash, DatumOption, ExUnitPrices, GovAction, GovActionId, - PParamsUpdate, PlutusData, PolicyId, PoolMetadata, PoolVotingThresholds, ProposalProcedure, - ProtocolVersion, RationalNumber, Relay, RewardAccount, ScriptHash, TransactionInput, - TransactionOutput, Value, Vote, + DRep, DRepVotingThresholds, DatumHash, DatumOption, ExUnitPrices, FieldedRewardAccount, + GovAction, GovActionId, PParamsUpdate, PlutusData, PolicyId, PoolMetadata, + PoolVotingThresholds, ProposalProcedure, ProtocolVersion, RationalNumber, Relay, + ScriptHash, TransactionInput, TransactionOutput, Value, Vote, }, localtxsubmission::{ primitives::{ @@ -38,10 +38,9 @@ use pallas_network::miniprotocols::localtxsubmission::{ Array, BabbageContextError, CollectError, ConwayCertPredFailure, ConwayContextError, ConwayDelegPredFailure, ConwayGovCertPredFailure, ConwayGovPredFailure, ConwayLedgerFailure, ConwayTxCert, ConwayUtxoWPredFailure, DeltaCoin, DisplayAddress, DisplayCoin, DisplayOSet, - DisplayPolicyId, DisplayRewardAccount, DisplayScriptHash, DisplayVotingProcedures, EpochNo, - FailureDescription, KeyHash, Mismatch, OHashMap, PlutusPurpose, SMaybe, SafeHash, - ShelleyPoolPredFailure, SlotNo, TagMismatchDescription, TxOutSource, Utxo, UtxoFailure, - UtxosFailure, VKey, ValidityInterval, + DisplayPolicyId, DisplayScriptHash, DisplayVotingProcedures, EpochNo, FailureDescription, + KeyHash, Mismatch, OHashMap, PlutusPurpose, SMaybe, SafeHash, ShelleyPoolPredFailure, SlotNo, + TagMismatchDescription, TxOutSource, Utxo, UtxoFailure, UtxosFailure, VKey, ValidityInterval, }; use super::haskells_show_string::haskell_show_string; @@ -626,26 +625,12 @@ where } } -impl HaskellDisplay for DisplayRewardAccount { +impl HaskellDisplay for FieldedRewardAccount { fn to_haskell_str(&self) -> String { - let network = if self.0[0] & 0b00000001 != 0 { - Network::Mainnet - } else { - Network::Testnet - }; - - let mut hash = [0; 28]; - hash.copy_from_slice(&self.0[1..29]); - let credential = if &self.0[0] & 0b00010000 != 0 { - StakeCredential::ScriptHash(hash.into()) - } else { - StakeCredential::AddrKeyhash(hash.into()) - }; - format!( "RewardAccount {{raNetwork = {}, raCredential = {}}}", - network.to_haskell_str(), - credential.to_haskell_str() + self.network.to_haskell_str(), + self.stake_credential.to_haskell_str() ) } } @@ -807,8 +792,8 @@ impl HaskellDisplay for GovAction { ) } TreasuryWithdrawals(a, b) => { - let data: KeyValuePairs = - a.iter().map(|(k, v)| (k.into(), v.into())).collect(); + let data: KeyValuePairs = + a.iter().map(|(k, v)| (k.clone(), v.into())).collect(); format!( "TreasuryWithdrawals {} {}", @@ -977,7 +962,7 @@ impl HaskellDisplay for ProposalProcedure { format!( "ProposalProcedure {{pProcDeposit = {}, pProcReturnAddr = {}, pProcGovAction = {}, pProcAnchor = {}}}", self.deposit.as_display_coin(), - self.return_addr.as_reward_account(), + self.return_addr.to_haskell_str(), self.gov_action.to_haskell_str(), self.anchor.to_haskell_str() ) @@ -1398,16 +1383,6 @@ impl AsProtocolVersion for ProtocolVersion { } } -trait AsRewardAccount { - fn as_reward_account(&self) -> String; -} - -impl AsRewardAccount for RewardAccount { - fn as_reward_account(&self) -> String { - DisplayRewardAccount(self.clone()).to_haskell_str() - } -} - impl AsFromList for Vec<&Option> { fn as_from_list(&self) -> String { let result = self @@ -2189,7 +2164,7 @@ impl HaskellDisplay TransactionInput, PolicyId, ConwayTxCert, - DisplayRewardAccount, + FieldedRewardAccount, Voter, ProposalProcedure, > @@ -2309,7 +2284,7 @@ impl HaskellDisplay for Certificate { pledge.as_display_coin(), cost.as_display_coin(), margin.to_haskell_str(), - reward_account.as_reward_account(), + reward_account.to_haskell_str(), pool_owners.as_key_hash(), relays.as_strict_seq(), pool_metadata.to_haskell_str() @@ -2423,11 +2398,7 @@ impl HaskellDisplay for DisplayOSet { let mut sorted_vec = self.0.deref().clone(); - sorted_vec.sort_by(|a, b| { - a.deposit - .cmp(&b.deposit) - .then_with(|| b.return_addr.cmp(&a.return_addr)) - }); + sorted_vec.sort(); format!( "OSet {{osSSeq = {}, osSet = {}}}", diff --git a/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs b/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs index 15760302b..ac2092c07 100644 --- a/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs +++ b/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs @@ -795,6 +795,38 @@ impl<'b, C> minicbor::Decode<'b, C> for CostModels { } } + +impl minicbor::Encode for FieldedRewardAccount { + fn encode( + &self, + e: &mut minicbor::Encoder, + _ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + let network: u8 = self.network.clone().into(); + + let hash = match &self.stake_credential { + StakeCredential::ScriptHash(hash) | StakeCredential::AddrKeyhash(hash) => hash, + }; + + let mut bytes: [u8; 29] = [0u8; 29]; + + bytes[0] = network; + bytes[1..].copy_from_slice(hash.as_ref()); + + e.bytes(&bytes)?; + Ok(()) + } +} + +impl<'b, C> minicbor::Decode<'b, C> for FieldedRewardAccount { + fn decode( + d: &mut minicbor::Decoder<'b>, + _ctx: &mut C, + ) -> Result { + Ok(d.bytes()?.into()) + } +} + #[cfg(test)] pub mod tests { use pallas_codec::minicbor; diff --git a/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs b/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs index 9478198cf..f2032e1a9 100644 --- a/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs +++ b/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs @@ -18,11 +18,11 @@ pub mod primitives; pub use primitives::{PoolMetadata, Relay}; use crate::miniprotocols::localtxsubmission::primitives::{ - CommitteeColdCredential, CommitteeHotCredential, ScriptRef, + CommitteeColdCredential, CommitteeHotCredential, ScriptRef, StakeCredential }; use crate::miniprotocols::Point; -use crate::miniprotocols::localtxsubmission::SMaybe; +use crate::miniprotocols::localtxsubmission::{Network, SMaybe}; use super::{Client, ClientError}; @@ -77,7 +77,7 @@ pub type Credential = StakeAddr; /// Updates to the protocol params as [in the Haskell sources](https://github.com/IntersectMBO/cardano-ledger/blob/d30a7ae828e802e98277c82e278e570955afc273/libs/cardano-ledger-core/src/Cardano/Ledger/Core/PParams.hs#L151) /// (via [`EraPParams`](https://github.com/IntersectMBO/cardano-ledger/blob/d30a7ae828e802e98277c82e278e570955afc273/libs/cardano-ledger-core/src/Cardano/Ledger/Core/PParams.hs#L255-L258)). -#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[derive(Encode, Decode, Debug, PartialEq, PartialOrd, Ord, Eq, Clone)] #[cbor(map)] pub struct PParamsUpdate { #[n(0)] @@ -297,7 +297,7 @@ pub struct ChainBlockNumber { pub block_number: u32, } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct RationalNumber { pub numerator: u64, pub denominator: u64, @@ -312,7 +312,7 @@ pub type ProtocolVersion = (ProtocolVersionMajor, ProtocolVersionMinor); pub type CostModel = Vec; -#[derive(Encode, Debug, PartialEq, Eq, Clone)] +#[derive(Encode, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] #[cbor(map)] pub struct CostModels { #[n(0)] @@ -328,7 +328,7 @@ pub struct CostModels { pub unknown: KeyValuePairs, } -#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct ExUnitPrices { #[n(0)] pub mem_price: PositiveInterval, @@ -337,7 +337,7 @@ pub struct ExUnitPrices { pub step_price: PositiveInterval, } -#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct ExUnits { #[n(0)] pub mem: u64, @@ -345,7 +345,7 @@ pub struct ExUnits { pub steps: u64, } /// Pool voting thresholds as [in the Haskell sources](https://github.com/IntersectMBO/cardano-ledger/blob/d30a7ae828e802e98277c82e278e570955afc273/eras/conway/impl/src/Cardano/Ledger/Conway/PParams.hs#L223-L229). -#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct PoolVotingThresholds { #[n(0)] pub motion_no_confidence: UnitInterval, @@ -360,7 +360,7 @@ pub struct PoolVotingThresholds { } /// DRrep voting thresholds as [in the Haskell sources](https://github.com/IntersectMBO/cardano-ledger/blob/d30a7ae828e802e98277c82e278e570955afc273/eras/conway/impl/src/Cardano/Ledger/Conway/PParams.hs#L295-L306). -#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct DRepVotingThresholds { #[n(0)] pub motion_no_confidence: UnitInterval, @@ -386,7 +386,9 @@ pub struct DRepVotingThresholds { /// Conway era protocol parameters, corresponding to [`ConwayPParams`](https://github.com/IntersectMBO/cardano-ledger/blob/d30a7ae828e802e98277c82e278e570955afc273/eras/conway/impl/src/Cardano/Ledger/Conway/PParams.hs#L512-L579) /// in the Haskell sources. -#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)] +/// @todo: Encoding should be handled manually, Encode derive won't be correct. +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] +#[cbor(map)] pub struct ProtocolParam { #[n(0)] pub minfee_a: Option, @@ -528,7 +530,7 @@ pub struct IndividualPoolStake { pub type PoolDistr = BTreeMap; /// Anchor as [in the Haskell sources](https://github.com/IntersectMBO/cardano-ledger/blob/d30a7ae828e802e98277c82e278e570955afc273/libs/cardano-ledger-core/src/Cardano/Ledger/BaseTypes.hs#L867-L870). -#[derive(Debug, Encode, Decode, PartialEq, Eq, Clone)] +#[derive(Debug, Encode, Decode, PartialEq, Eq, Clone, PartialOrd, Ord)] pub struct Anchor { #[n(0)] pub url: String, @@ -537,7 +539,7 @@ pub struct Anchor { } /// Constitution as defined [in the Haskell sources](https://github.com/IntersectMBO/cardano-ledger/blob/d30a7ae828e802e98277c82e278e570955afc273/eras/conway/impl/src/Cardano/Ledger/Conway/Governance/Procedures.hs#L884-L887). -#[derive(Debug, Encode, Decode, Eq, PartialEq, Clone)] +#[derive(Debug, Encode, Decode, Eq, PartialEq, Clone, PartialOrd, Ord)] pub struct Constitution { #[n(0)] pub anchor: Anchor, @@ -557,16 +559,41 @@ pub enum Vote { Abstain, } -pub type RewardAccount = Bytes; +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub struct FieldedRewardAccount{ + pub network: Network, pub stake_credential: StakeCredential} + +impl From<&[u8]> for FieldedRewardAccount { + fn from(bytes: &[u8]) -> Self { + + let network = if bytes[0] & 0b00000001 != 0 { + Network::Mainnet + } else { + Network::Testnet + }; + + let mut hash = [0; 28]; + hash.copy_from_slice(&bytes[1..29]); + let stake_credential = if &bytes[0] & 0b00010000 != 0 { + StakeCredential::ScriptHash(hash.into()) + } else { + StakeCredential::AddrKeyhash(hash.into()) + }; + + + FieldedRewardAccount{network, stake_credential} + } +} + pub type ScriptHash = Hash<28>; /// Governance action as defined [in the Haskell sources](https://github.com/IntersectMBO/cardano-ledger/blob/d30a7ae828e802e98277c82e278e570955afc273/eras/conway/impl/src/Cardano/Ledger/Conway/Governance/Procedures.hs#L785-L824). -#[derive(Debug, Eq, PartialEq, Clone)] +#[derive(Debug, Eq, PartialEq, Clone, PartialOrd, Ord)] #[allow(clippy::large_enum_variant)] pub enum GovAction { ParameterChange(Option, PParamsUpdate, Option), HardForkInitiation(Option, ProtocolVersion), - TreasuryWithdrawals(KeyValuePairs, Option), + TreasuryWithdrawals(KeyValuePairs, Option), NoConfidence(Option), UpdateCommittee( Option, @@ -579,12 +606,12 @@ pub enum GovAction { } /// Proposal procedure state as defined [in the Haskell sources](https://github.com/IntersectMBO/cardano-ledger/blob/d30a7ae828e802e98277c82e278e570955afc273/eras/conway/impl/src/Cardano/Ledger/Conway/Governance/Procedures.hs#L476-L481 -#[derive(Debug, Encode, Decode, Eq, PartialEq, Clone)] +#[derive(Debug, Encode, Decode, Eq, PartialEq, Clone, PartialOrd, Ord)] pub struct ProposalProcedure { #[n(0)] pub deposit: Coin, #[n(1)] - pub return_addr: RewardAccount, + pub return_addr: FieldedRewardAccount, #[n(2)] pub gov_action: GovAction, #[n(3)] diff --git a/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs b/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs index 104e544cc..ff5264892 100644 --- a/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs +++ b/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs @@ -1,7 +1,7 @@ // Material brought from `pallas-primitives` // TODO: Refactor in order to avoid repetition. use crate::miniprotocols::localstate::queries_v16::{ - Anchor, AssetName, Coin, DRep, Epoch, PolicyId, PoolMetadata, Relay, RewardAccount, ScriptHash, + Anchor, AssetName, Coin, DRep, Epoch, PolicyId, PoolMetadata, Relay, FieldedRewardAccount, ScriptHash, UnitInterval, }; pub use pallas_codec::utils::KeyValuePairs; @@ -37,7 +37,7 @@ pub enum Certificate { pledge: Coin, cost: Coin, margin: UnitInterval, - reward_account: RewardAccount, + reward_account: FieldedRewardAccount, pool_owners: Set, relays: Vec, pool_metadata: Nullable, diff --git a/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs b/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs index 949779c37..3667fc63e 100644 --- a/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs +++ b/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs @@ -3,8 +3,7 @@ use thiserror::Error; use super::primitives::{Certificate, Credential, Language, StakeCredential, Voter}; use crate::miniprotocols::localstate::queries_v16::{ Anchor, GovAction, GovActionId, PolicyId, ProposalProcedure, ProtocolVersion, ScriptHash, - TransactionInput, TransactionOutput, Value, Vote, -}; + TransactionInput, TransactionOutput, Value, Vote, FieldedRewardAccount}; pub use crate::miniprotocols::localstate::queries_v16::{Coin, ExUnits, TaggedSet}; use crate::multiplexer; use pallas_codec::minicbor::{self, Decode, Encode}; @@ -103,7 +102,7 @@ pub type PlutusPurposeItem = PlutusPurpose< TransactionInput, PolicyId, ConwayTxCert, - DisplayRewardAccount, + FieldedRewardAccount, Voter, ProposalProcedure, >; @@ -263,7 +262,7 @@ pub struct Utxo(pub OHashMap); #[derive(Debug, PartialEq, Eq, Clone)] pub struct OHashMap(pub Vec<(K, V)>); -#[derive(Encode, Decode, Debug, Clone, Eq, PartialEq)] +#[derive(Encode, Decode, Debug, Clone, Eq, PartialEq, PartialOrd, Ord)] #[cbor(index_only)] pub enum Network { #[n(0)] @@ -272,19 +271,19 @@ pub enum Network { Mainnet, } +impl From for u8 { + fn from(value: Network) -> u8 { + match value { + Network::Mainnet => 1, + Network::Testnet => 0, + } + } +} #[derive(Debug, Decode, Encode, Clone, Eq, PartialEq)] #[cbor(transparent)] pub struct DeltaCoin(#[n(0)] pub i32); -#[derive(Debug, Decode, Encode, Clone, Eq, PartialEq)] -#[cbor(transparent)] -pub struct DisplayRewardAccount(#[n(0)] pub Bytes); -impl From<&Bytes> for DisplayRewardAccount { - fn from(bytes: &Bytes) -> Self { - DisplayRewardAccount(bytes.to_owned()) - } -} pub type Slot = u64; @@ -318,7 +317,7 @@ pub enum UtxoFailure { #[n(7)] WrongNetwork(#[n(0)] Network, #[n(1)] Set), #[n(8)] - WrongNetworkWithdrawal(#[n(0)] Network, #[n(1)] Set), + WrongNetworkWithdrawal(#[n(0)] Network, #[n(1)] Set), #[n(9)] OutputTooSmallUTxO(#[n(0)] Array), #[n(10)] @@ -464,7 +463,11 @@ pub enum ConwayLedgerFailure { #[cbor(flat)] pub enum ConwayCertsPredFailure { #[n(0)] +<<<<<<< HEAD WithdrawalsNotInRewardsCERTS(#[n(0)] OHashMap), +======= + WithdrawalsNotInRewardsCERTS(#[n(0)] OHashMap), +>>>>>>> ff7db11 (Match ProposalProcedure ordering with Haskell implementation) #[n(1)] CertFailure(#[n(0)] ConwayCertPredFailure), } @@ -542,9 +545,9 @@ pub enum ConwayGovPredFailure { #[n(1)] MalformedProposal(#[n(0)] GovAction), #[n(2)] - ProposalProcedureNetworkIdMismatch(#[n(0)] DisplayRewardAccount, #[n(1)] Network), + ProposalProcedureNetworkIdMismatch(#[n(0)] FieldedRewardAccount, #[n(1)] Network), #[n(3)] - TreasuryWithdrawalsNetworkIdMismatch(#[n(0)] Set, #[n(1)] Network), + TreasuryWithdrawalsNetworkIdMismatch(#[n(0)] Set, #[n(1)] Network), #[n(4)] ProposalDepositIncorrect(#[n(0)] DisplayCoin, #[n(1)] DisplayCoin), #[n(5)] @@ -577,9 +580,9 @@ pub enum ConwayGovPredFailure { #[n(15)] ZeroTreasuryWithdrawals(#[n(0)] GovAction), #[n(16)] - ProposalReturnAccountDoesNotExist(#[n(0)] DisplayRewardAccount), + ProposalReturnAccountDoesNotExist(#[n(0)] FieldedRewardAccount), #[n(17)] - TreasuryWithdrawalReturnAccountsDoNotExist(#[n(0)] Vec), + TreasuryWithdrawalReturnAccountsDoNotExist(#[n(0)] Vec), } /// Reject reason. It can be a pair of an era number and a sequence of errors, From bf2b5e8c1b4d8ddc63170a3925fbbd317016e0d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Mon, 17 Mar 2025 17:05:12 +0300 Subject: [PATCH 03/13] Implement HaskellDisplay for DecoderError --- pallas-hardano/src/display/haskell_display.rs | 28 ++++++++++- pallas-hardano/src/display/haskell_error.rs | 46 ++++++++++++++++--- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/pallas-hardano/src/display/haskell_display.rs b/pallas-hardano/src/display/haskell_display.rs index 8a6eb6ce4..7cfd1edd2 100644 --- a/pallas-hardano/src/display/haskell_display.rs +++ b/pallas-hardano/src/display/haskell_display.rs @@ -1,5 +1,6 @@ use std::{ collections::{BTreeSet, HashMap}, + fmt::format, net::Ipv6Addr, ops::Deref, }; @@ -43,7 +44,10 @@ use pallas_network::miniprotocols::localtxsubmission::{ TagMismatchDescription, TxOutSource, Utxo, UtxoFailure, UtxosFailure, VKey, ValidityInterval, }; -use super::haskells_show_string::haskell_show_string; +use super::{ + haskell_error::{DecoderError, DeserialiseFailure}, + haskells_show_string::haskell_show_string, +}; /// Trait used to generated Haskell-like string representation of a type. pub trait HaskellDisplay { @@ -521,6 +525,28 @@ impl HaskellDisplay for BabbageContextError { } } +impl HaskellDisplay for DecoderError { + fn to_haskell_str(&self) -> String { + match self { + DecoderError::DeserialiseFailure(str, failure) => format!( + "DecoderErrorDeserialiseFailure {} {}", + str.to_haskell_str(), + failure.to_haskell_str_p() + ), + _ => format!("DecoderError ({:?})", self), + } + } +} +impl HaskellDisplay for DeserialiseFailure { + fn to_haskell_str(&self) -> String { + format!( + "DeserialiseFailure {} ({})", + self.0.to_haskell_str(), + self.1.to_haskell_str() + ) + } +} + impl HaskellDisplay for TxOutSource { fn to_haskell_str(&self) -> String { use TxOutSource::*; diff --git a/pallas-hardano/src/display/haskell_error.rs b/pallas-hardano/src/display/haskell_error.rs index b54e0d898..62be7d7a5 100644 --- a/pallas-hardano/src/display/haskell_error.rs +++ b/pallas-hardano/src/display/haskell_error.rs @@ -12,11 +12,22 @@ pub fn wrap_error_response(error: TxValidationError) -> TxSubmitFail { )) } -/// Generates Haskell identical string for the error response +/// Generates Haskell 'identical' string for the error response pub fn as_node_submit_error(error: TxValidationError) -> String { serde_json::to_string(&wrap_error_response(error)).unwrap() } +/// Generates Haskell 'similar' string for the error response in case of decode failure +/// Only difference will be the provided decode failure message, Rust vs Haskell +pub fn as_cbor_decode_failure(message: String, position: u64) -> String { + let inner_errors = vec![DecoderError::DeserialiseFailure( + "Shelley Tx".to_string(), + DeserialiseFailure(position, message), + )]; + let error = TxSubmitFail::TxSubmitFail(TxCmdError::TxReadError(inner_errors)); + serde_json::to_string(&error).unwrap() +} + pub fn serialize_error(error: TxValidationError) -> serde_json::Value { serde_json::to_value(wrap_error_response(error)).unwrap() } @@ -37,6 +48,7 @@ pub enum TxSubmitFail { #[serde(tag = "tag", content = "contents")] pub enum TxCmdError { SocketEnvError(String), + #[serde(serialize_with = "use_haskell_display", rename = "TxCmdTxReadError")] TxReadError(Vec), TxCmdTxSubmitValidationError(TxValidationErrorInCardanoMode), } @@ -57,9 +69,21 @@ pub struct EraMismatch { other: String, // Era of the block, header, transaction, or query. } -/// TODO: Implement DecoderError errors from the Haskell codebase. -/// Lots of errors, skipping for now. https://github.com/IntersectMBO/cardano-base/blob/391a2c5cfd30d2234097e000dbd8d9db21ef94d7/cardano-binary/src/Cardano/Binary/FromCBOR.hs#L90 -type DecoderError = String; +/// https://github.com/IntersectMBO/cardano-base/blob/391a2c5cfd30d2234097e000dbd8d9db21ef94d7/cardano-binary/src/Cardano/Binary/FromCBOR.hs#L90 +#[derive(Debug, Serialize)] +pub enum DecoderError { + CanonicityViolation(String), + Custom(String, String), + DeserialiseFailure(String, DeserialiseFailure), + EmptyList(String), + Leftover(String, Vec), + SizeMismatch(String, u64, u64), + UnknownTag(String, u8), + Void, +} +/// https://hackage.haskell.org/package/serialise-0.2.6.1/docs/Codec-Serialise.html#t:DeserialiseFailure +#[derive(Debug, Serialize)] +pub struct DeserialiseFailure(pub u64, pub String); // // Haskell JSON serializations @@ -83,7 +107,6 @@ enum TxValidationErrorJson { } /// This is copy of ApplyTxError from pallas-network/src/miniprotocols/localtxsubmission/primitives.rs for Haskell json serialization - #[derive(Debug, Clone, Eq, PartialEq, Serialize)] #[serde(remote = "ApplyTxError")] struct ApplyTxErrorJson( @@ -108,9 +131,10 @@ enum ShelleyBasedEraJson { Conway, } -fn use_haskell_display(fails: &[ConwayLedgerFailure], serializer: S) -> Result +fn use_haskell_display(fails: &[T], serializer: S) -> Result where S: Serializer, + T: HaskellDisplay, { let fails_str = fails.iter().map(|fail| fail.to_haskell_str()); serializer.collect_seq(fails_str) @@ -122,7 +146,15 @@ where fn test_submit_api_serialization() { let error = decode_error("81820681820764f0aab883"); - assert_eq!("{\"tag\":\"TxSubmitFail\",\"contents\":{\"tag\":\"TxCmdTxSubmitValidationError\",\"contents\":{\"tag\":\"TxValidationErrorInCardanoMode\",\"contents\":{\"kind\":\"ShelleyTxValidationError\",\"error\":[\"ConwayMempoolFailure \\\"\\\\175619\\\"\"],\"era\":\"ShelleyBasedEraConway\"}}}}", as_node_submit_error(error)); + assert_eq!("{\"tag\":\"TxSubmitFail\",\"contents\":{\"tag\":\"TxCmdTxSubmitValidationError\",\"contents\":{\"tag\":\"TxValidationErrorInCardanoMode\",\"contents\":{\"kind\":\"ShelleyTxValidationError\",\"error\":[\"ConwayMempoolFailure \\\"\\\\175619\\\"\"],\"era\":\"ShelleyBasedEraConway\"}}}}", + as_node_submit_error(error)); +} + +#[test] +#[allow(non_snake_case)] +fn test_submit_api_decode_failure() { + assert_eq!( "{\"tag\":\"TxSubmitFail\",\"contents\":{\"tag\":\"TxCmdTxReadError\",\"contents\":[\"DecoderErrorDeserialiseFailure \\\"Shelley Tx\\\" (DeserialiseFailure 0 (\\\"expected list len or indef\\\"))\"]}}", + as_cbor_decode_failure("expected list len or indef".to_string(), 0)); } #[cfg(test)] From 8071e1b3d178d9bf31060b4c8b90c6ce83263a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Wed, 19 Mar 2025 18:39:23 +0300 Subject: [PATCH 04/13] Use BigInt where i32 and i64 is not enough --- pallas-hardano/src/display/haskell_display.rs | 61 +++++++++++++------ .../localtxsubmission/protocol.rs | 16 ++--- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/pallas-hardano/src/display/haskell_display.rs b/pallas-hardano/src/display/haskell_display.rs index 7cfd1edd2..9c4f98d0b 100644 --- a/pallas-hardano/src/display/haskell_display.rs +++ b/pallas-hardano/src/display/haskell_display.rs @@ -1,6 +1,5 @@ use std::{ collections::{BTreeSet, HashMap}, - fmt::format, net::Ipv6Addr, ops::Deref, }; @@ -20,11 +19,11 @@ use pallas_crypto::hash::{Hash, Hasher}; use pallas_network::miniprotocols::{ handshake::NetworkMagic, localstate::queries_v16::{ - primitives::Bytes, Anchor, AssetName, BoundedBytes, Constitution, CostModel, CostModels, - DRep, DRepVotingThresholds, DatumHash, DatumOption, ExUnitPrices, FieldedRewardAccount, - GovAction, GovActionId, PParamsUpdate, PlutusData, PolicyId, PoolMetadata, - PoolVotingThresholds, ProposalProcedure, ProtocolVersion, RationalNumber, Relay, - ScriptHash, TransactionInput, TransactionOutput, Value, Vote, + primitives::Bytes, Anchor, AssetName, BigInt, BoundedBytes, Constitution, CostModel, + CostModels, DRep, DRepVotingThresholds, DatumHash, DatumOption, ExUnitPrices, + FieldedRewardAccount, GovAction, GovActionId, PParamsUpdate, PlutusData, PolicyId, + PoolMetadata, PoolVotingThresholds, ProposalProcedure, ProtocolVersion, RationalNumber, + Relay, ScriptHash, TransactionInput, TransactionOutput, Value, Vote, }, localtxsubmission::{ primitives::{ @@ -351,8 +350,8 @@ impl HaskellDisplay for UtxoFailure { ), MaxTxSizeUTxO(actual, max) => format!( "(MaxTxSizeUTxO {} {})", - actual.to_haskell_str_p(), - max.to_haskell_str_p() + actual.to_haskell_str(), + max.to_haskell_str() ), InputSetEmptyUTxO => "InputSetEmptyUTxO".to_string(), FeeTooSmallUTxO(required, provided) => format!( @@ -1076,6 +1075,20 @@ impl HaskellDisplay for i64 { } } +impl HaskellDisplay for i128 { + fn to_haskell_str(&self) -> String { + self.to_string() + } + + fn to_haskell_str_p(&self) -> String { + if *self >= 0 { + self.to_string() + } else { + format!("({})", self) + } + } +} + impl HaskellDisplay for u8 { fn to_haskell_str(&self) -> String { format!("{self}") @@ -1534,6 +1547,25 @@ impl HaskellDisplay for u64 { } } +impl HaskellDisplay for BigInt { + fn to_haskell_str(&self) -> String { + use BigInt::*; + + match self { + Int(i) => { + let value: i128 = i.0.into(); + value.to_haskell_str_p() + }, + BigNInt(bb) => { + format!("BigNInt {}", bb.to_haskell_str_p()) + } + BigUInt(bb) => { + format!("BigUInt {}", bb.to_haskell_str_p()) + } + } + } +} + impl HaskellDisplay for String { fn to_haskell_str(&self) -> String { self.as_text() @@ -2093,21 +2125,12 @@ impl AsDatumHash for DatumHash { impl HaskellDisplay for PlutusData { fn to_haskell_str(&self) -> String { - use pallas_network::miniprotocols::localstate::queries_v16::BigInt as Big; use PlutusData::*; match self { Constr(constr) => constr.fields.to_haskell_str(), Map(key_value_pairs) => key_value_pairs.to_haskell_str().to_string(), - BigInt(big_int) => match big_int { - Big::Int(i) => format!("BigInt Int {}", i.to_haskell_str()), - Big::BigNInt(bb) => { - format!("BigNInt {}", bb.to_haskell_str()) - } - Big::BigUInt(bb) => { - format!("BigUInt {}", bb.to_haskell_str()) - } - }, + BigInt(big_int) => big_int.to_haskell_str(), BoundedBytes(bb) => bb.to_haskell_str(), Array(arr) => format!("Array {}", arr.to_haskell_str()), } @@ -2141,7 +2164,7 @@ where impl HaskellDisplay for Int { fn to_haskell_str(&self) -> String { - format!("Int {}", self.0) + format!("{}", self.0) } } diff --git a/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs b/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs index 3667fc63e..d63e2321b 100644 --- a/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs +++ b/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs @@ -2,8 +2,8 @@ use thiserror::Error; use super::primitives::{Certificate, Credential, Language, StakeCredential, Voter}; use crate::miniprotocols::localstate::queries_v16::{ - Anchor, GovAction, GovActionId, PolicyId, ProposalProcedure, ProtocolVersion, ScriptHash, - TransactionInput, TransactionOutput, Value, Vote, FieldedRewardAccount}; + Anchor, BigInt, FieldedRewardAccount, GovAction, GovActionId, PolicyId, ProposalProcedure, ProtocolVersion, ScriptHash, + TransactionInput, TransactionOutput, Value, Vote, }; pub use crate::miniprotocols::localstate::queries_v16::{Coin, ExUnits, TaggedSet}; use crate::multiplexer; use pallas_codec::minicbor::{self, Decode, Encode}; @@ -281,9 +281,7 @@ impl From for u8 { } #[derive(Debug, Decode, Encode, Clone, Eq, PartialEq)] #[cbor(transparent)] -pub struct DeltaCoin(#[n(0)] pub i32); - - +pub struct DeltaCoin(#[n(0)] pub BigInt); pub type Slot = u64; @@ -307,7 +305,7 @@ pub enum UtxoFailure { #[n(2)] OutsideValidityIntervalUTxO(#[n(0)] ValidityInterval, #[n(1)] Slot), #[n(3)] - MaxTxSizeUTxO(#[n(0)] i64, #[n(1)] i64), + MaxTxSizeUTxO(#[n(0)] BigInt, #[n(1)] BigInt), #[n(4)] InputSetEmptyUTxO, #[n(5)] @@ -337,7 +335,7 @@ pub enum UtxoFailure { #[n(17)] OutsideForecast(#[n(0)] Slot), #[n(18)] - TooManyCollateralInputs(#[n(0)] u16, #[n(1)] u16), + TooManyCollateralInputs(#[n(0)] u64, #[n(1)] u64), #[n(19)] NoCollateralInputs, #[n(20)] @@ -463,11 +461,7 @@ pub enum ConwayLedgerFailure { #[cbor(flat)] pub enum ConwayCertsPredFailure { #[n(0)] -<<<<<<< HEAD - WithdrawalsNotInRewardsCERTS(#[n(0)] OHashMap), -======= WithdrawalsNotInRewardsCERTS(#[n(0)] OHashMap), ->>>>>>> ff7db11 (Match ProposalProcedure ordering with Haskell implementation) #[n(1)] CertFailure(#[n(0)] ConwayCertPredFailure), } From 4624c89440205ea6223d1ff5451a16ce2cc17595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Sat, 24 May 2025 01:04:58 +0300 Subject: [PATCH 05/13] fix get_current_pparams miniprotocol query --- .../localstate/queries_v16/mod.rs | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs b/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs index f2032e1a9..edd53b71c 100644 --- a/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs +++ b/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs @@ -454,6 +454,72 @@ pub struct ProtocolParam { pub minfee_refscript_cost_per_byte: Option, } +#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)] +pub struct CurrentProtocolParam { + #[n(0)] + pub minfee_a: Option, + #[n(1)] + pub minfee_b: Option, + #[n(2)] + pub max_block_body_size: Option, + #[n(3)] + pub max_transaction_size: Option, + #[n(4)] + pub max_block_header_size: Option, + #[n(5)] + pub key_deposit: Option, + #[n(6)] + pub pool_deposit: Option, + #[n(7)] + pub maximum_epoch: Option, + #[n(8)] + pub desired_number_of_stake_pools: Option, + #[n(9)] + pub pool_pledge_influence: Option, + #[n(10)] + pub expansion_rate: Option, + #[n(11)] + pub treasury_growth_rate: Option, + #[n(12)] + pub protocol_version: Option, + #[n(13)] + pub min_pool_cost: Option, + #[n(14)] + pub ada_per_utxo_byte: Option, + #[n(15)] + pub cost_models_for_script_languages: Option, + #[n(16)] + pub execution_costs: Option, + #[n(17)] + pub max_tx_ex_units: Option, + #[n(18)] + pub max_block_ex_units: Option, + #[n(19)] + pub max_value_size: Option, + #[n(20)] + pub collateral_percentage: Option, + #[n(21)] + pub max_collateral_inputs: Option, + #[n(22)] + pub pool_voting_thresholds: Option, + #[n(23)] + pub drep_voting_thresholds: Option, + #[n(24)] + pub min_committee_size: Option, + #[n(25)] + pub committee_term_limit: Option, + #[n(26)] + pub governance_action_validity_period: Option, + #[n(27)] + pub governance_action_deposit: Option, + #[n(28)] + pub drep_deposit: Option, + #[n(29)] + pub drep_inactivity_period: Option, + #[n(30)] + pub minfee_refscript_cost_per_byte: Option, +} + pub type StakeDistribution = KeyValuePairs; /// Tuple struct based on `BTreeSet` which uses the "Set" CBOR tag. @@ -1230,7 +1296,7 @@ block_query_no_args! { #[doc = "Get the current protocol parameters"] get_current_pparams, GetCurrentPParams, - ProtocolParam, + CurrentProtocolParam, } block_query_no_args! { From fdd517ab92e2c51de1e32a3f3f0af0c95dab9842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Tue, 27 May 2025 11:24:21 +0300 Subject: [PATCH 06/13] The fix for a breaking change --- pallas-hardano/src/display/haskell_display.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pallas-hardano/src/display/haskell_display.rs b/pallas-hardano/src/display/haskell_display.rs index 9c4f98d0b..b5564f373 100644 --- a/pallas-hardano/src/display/haskell_display.rs +++ b/pallas-hardano/src/display/haskell_display.rs @@ -360,9 +360,8 @@ impl HaskellDisplay for UtxoFailure { provided.to_haskell_str_p() ), ValueNotConservedUTxO(required, provided) => format!( - "(ValueNotConservedUTxO {} {})", - required.to_haskell_str_p(), - provided.to_haskell_str_p() + "(ValueNotConservedUTxO {})", + Mismatch(required.to_owned(), provided.to_owned()).to_haskell_str_p() ), OutputTooSmallUTxO(outputs) => { format!("(OutputTooSmallUTxO {})", outputs.to_haskell_str_p()) From 613032475309282d3e6820254a840aba0dce9b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Tue, 27 May 2025 16:02:37 +0300 Subject: [PATCH 07/13] The fix for a breaking changes that comes with the new node --- pallas-hardano/src/display/haskell_display.rs | 176 ++++++++++-------- .../miniprotocols/localtxsubmission/codec.rs | 1 - 2 files changed, 103 insertions(+), 74 deletions(-) diff --git a/pallas-hardano/src/display/haskell_display.rs b/pallas-hardano/src/display/haskell_display.rs index b5564f373..ea86658cc 100644 --- a/pallas-hardano/src/display/haskell_display.rs +++ b/pallas-hardano/src/display/haskell_display.rs @@ -74,18 +74,10 @@ impl HaskellDisplay for ConwayLedgerFailure { format!("ConwayWdrlNotDelegatedToDRep {}", v.to_haskell_str_p()) } TreasuryValueMismatch(c1, c2) => { - format!( - "ConwayTreasuryValueMismatch {} {}", - c1.to_haskell_str_p(), - c2.to_haskell_str_p() - ) + format!("ConwayTreasuryValueMismatch ({})", as_mismatch(c1, c2),) } TxRefScriptsSizeTooBig(s1, s2) => { - format!( - "ConwayTxRefScriptsSizeTooBig {} {}", - s1.to_haskell_str_p(), - s2.to_haskell_str_p() - ) + format!("ConwayTxRefScriptsSizeTooBig ({})", as_mismatch(s2, s1)) } MempoolFailure(e) => format!("ConwayMempoolFailure {}", e.to_haskell_str()), U8(v) => format!("U8 {v}"), @@ -104,22 +96,18 @@ impl HaskellDisplay for ConwayGovCertPredFailure { DRepNotRegistered(cred) => { format!("ConwayDRepNotRegistered {}", cred.to_haskell_str_p()) } - DRepIncorrectDeposit(expected, actual) => format!( - "ConwayDRepIncorrectDeposit {} {}", - expected.to_haskell_str_p(), - actual.to_haskell_str_p() - ), + DRepIncorrectDeposit(c1, c2) => { + format!("ConwayDRepIncorrectDeposit ({})", as_mismatch(c2, c1)) + } CommitteeHasPreviouslyResigned(cred) => { format!( "ConwayCommitteeHasPreviouslyResigned {}", cred.to_haskell_str_p() ) } - DRepIncorrectRefund(expected, actual) => format!( - "ConwayDRepIncorrectRefund {} {}", - expected.to_haskell_str_p(), - actual.to_haskell_str_p() - ), + DRepIncorrectRefund(v1, v2) => { + format!("ConwayDRepIncorrectRefund ({})", as_mismatch(v2, v1)) + } CommitteeIsUnknown(cred) => { format!("ConwayCommitteeIsUnknown {}", cred.to_haskell_str_p()) } @@ -203,13 +191,15 @@ impl HaskellDisplay for ConwayUtxoWPredFailure { format!("(ScriptWitnessNotValidatingUTXOW {})", e.to_haskell_str_p()) } MissingTxBodyMetadataHash(b) => { - format!("(MissingTxBodyMetadataHash ({}))", b.as_aux_data_hash()) + format!("(MissingTxBodyMetadataHash ({}))", b.as_tx_aux_data_hash()) } - MissingTxMetadata(e) => format!("(MissingTxMetadata ({}))", e.as_aux_data_hash()), + MissingTxMetadata(e) => format!("(MissingTxMetadata ({}))", e.as_tx_aux_data_hash()), ConflictingMetadataHash(e1, e2) => format!( - "(ConflictingMetadataHash ({}) ({}))", - e1.as_aux_data_hash(), - e2.as_aux_data_hash() + "(ConflictingMetadataHash ({}))", + as_mismatch( + &e2.as_tx_aux_data_hash().as_is(), + &e1.as_tx_aux_data_hash().as_is() + ) ), InvalidMetadata() => "InvalidMetadata".to_string(), ExtraneousScriptWitnessesUTXOW(vec) => { @@ -229,11 +219,9 @@ impl HaskellDisplay for ConwayUtxoWPredFailure { e1.to_haskell_str_p(), e2.to_haskell_str_p() ), - PPViewHashesDontMatch(h1, h2) => format!( - "(PPViewHashesDontMatch {} {})", - h1.to_haskell_str_p(), - h2.to_haskell_str_p() - ), + PPViewHashesDontMatch(h1, h2) => { + format!("(PPViewHashesDontMatch ({}))", as_mismatch(h2, h1)) + } UnspendableUTxONoDatumHash(e) => { format!("(UnspendableUTxONoDatumHash {})", e.to_haskell_str_p()) } @@ -273,11 +261,7 @@ impl HaskellDisplay for ConwayGovPredFailure { ) } ProposalDepositIncorrect(c1, c2) => { - format!( - "ProposalDepositIncorrect {} {}", - c1.to_haskell_str_p(), - c2.to_haskell_str_p() - ) + format!("ProposalDepositIncorrect ({})", as_mismatch(c2, c1)) } DisallowedVoters(v) => { format!("DisallowedVoters {}", v.to_haskell_str_p()) @@ -296,10 +280,12 @@ impl HaskellDisplay for ConwayGovPredFailure { } ProposalCantFollow(a, p1, p2) => { format!( - "ProposalCantFollow {} ({}) ({})", + "ProposalCantFollow {} ({})", a.to_haskell_str_p(), - p1.as_protocol_version(), - p2.as_protocol_version() + as_mismatch( + &p2.as_protocol_version().as_is(), + &p1.as_protocol_version().as_is() + ) ) } InvalidPolicyHash(maybe1, maybe2) => { @@ -348,17 +334,9 @@ impl HaskellDisplay for UtxoFailure { interval.to_haskell_str(), slot.as_slot_no() ), - MaxTxSizeUTxO(actual, max) => format!( - "(MaxTxSizeUTxO {} {})", - actual.to_haskell_str(), - max.to_haskell_str() - ), + MaxTxSizeUTxO(s1, s2) => format!("(MaxTxSizeUTxO ({}))", as_mismatch(s2, s1)), InputSetEmptyUTxO => "InputSetEmptyUTxO".to_string(), - FeeTooSmallUTxO(required, provided) => format!( - "(FeeTooSmallUTxO {} {})", - required.to_haskell_str_p(), - provided.to_haskell_str_p() - ), + FeeTooSmallUTxO(c1, c2) => format!("(FeeTooSmallUTxO ({}))", as_mismatch(c1, c2)), ValueNotConservedUTxO(required, provided) => format!( "(ValueNotConservedUTxO {})", Mismatch(required.to_owned(), provided.to_owned()).to_haskell_str_p() @@ -375,11 +353,7 @@ impl HaskellDisplay for UtxoFailure { required.to_haskell_str_p() ), ScriptsNotPaidUTxO(utxo) => format!("(ScriptsNotPaidUTxO {})", utxo.to_haskell_str_p()), - ExUnitsTooBigUTxO(provided, max) => format!( - "(ExUnitsTooBigUTxO {} {})", - provided.to_haskell_str_p(), - max.to_haskell_str_p() - ), + ExUnitsTooBigUTxO(u1, u2) => format!("(ExUnitsTooBigUTxO ({}))", as_mismatch(u1, u2)), WrongNetwork(network, addrs) => format!( "(WrongNetwork {} {})", network.to_haskell_str(), @@ -396,13 +370,11 @@ impl HaskellDisplay for UtxoFailure { } NoCollateralInputs => "NoCollateralInputs".to_string(), TooManyCollateralInputs(actual, max) => { - format!("(TooManyCollateralInputs {actual} {max})") + format!("(TooManyCollateralInputs ({}))", as_mismatch(actual, max)) + } + WrongNetworkInTxBody(n1, n2) => { + format!("(WrongNetworkInTxBody ({}))", as_mismatch(n1, n2)) } - WrongNetworkInTxBody(expected, actual) => format!( - "(WrongNetworkInTxBody {} {})", - expected.to_haskell_str(), - actual.to_haskell_str() - ), IncorrectTotalCollateralField(actual, provided) => format!( "(IncorrectTotalCollateralField {} {})", actual.to_haskell_str_p(), @@ -641,14 +613,32 @@ where T: HaskellDisplay, { fn to_haskell_str(&self) -> String { - format!( - "Mismatch {{mismatchSupplied = {}, mismatchExpected = {}}}", - self.0.to_haskell_str(), - self.1.to_haskell_str() - ) + as_mismatch(&self.1, &self.0) } } +pub fn as_mismatch(expected: &T, supplied: &T) -> String +where + T: HaskellDisplay, +{ + format!( + "Mismatch {{mismatchSupplied = {}, mismatchExpected = {}}}", + supplied.to_haskell_str(), + expected.to_haskell_str() + ) +} + +pub fn as_mismatch_p(expected: &T, supplied: &T) -> String +where + T: HaskellDisplay, +{ + format!( + "Mismatch {{mismatchSupplied = {}, mismatchExpected = {}}}", + supplied.to_haskell_str_p(), + expected.to_haskell_str_p() + ) +} + impl HaskellDisplay for FieldedRewardAccount { fn to_haskell_str(&self) -> String { format!( @@ -1237,6 +1227,10 @@ trait AsVKey { fn as_vkey(&self) -> String; } +trait AsVrfKeyHash { + fn as_vrf_key_hash(&self) -> String; +} + trait AsScriptHashObject { fn as_script_hash_obj(&self) -> String; } @@ -1337,6 +1331,13 @@ where } } +impl AsVrfKeyHash for [u8] { + fn as_vrf_key_hash(&self) -> String { + let hex = hex::encode(self); + format!("VRFVerKeyHash {{unVRFVerKeyHash = \"{}\"}}", hex) + } +} + impl AsKeyHash for [u8] { fn as_key_hash(&self) -> String { let hex = hex::encode(self); @@ -1550,11 +1551,28 @@ impl HaskellDisplay for BigInt { fn to_haskell_str(&self) -> String { use BigInt::*; + match self { + Int(i) => { + let value: i128 = i.0.into(); + value.to_haskell_str() + } + BigNInt(bb) => { + format!("BigNInt {}", bb.to_haskell_str()) + } + BigUInt(bb) => { + format!("BigUInt {}", bb.to_haskell_str()) + } + } + } + + fn to_haskell_str_p(&self) -> String { + use BigInt::*; + match self { Int(i) => { let value: i128 = i.0.into(); value.to_haskell_str_p() - }, + } BigNInt(bb) => { format!("BigNInt {}", bb.to_haskell_str_p()) } @@ -2052,7 +2070,7 @@ impl HaskellDisplay for Pointer { fn to_haskell_str(&self) -> String { format!( "Ptr ({}) ({}) ({})", - self.slot().as_slot_no(), + self.slot().as_slot_no32(), self.tx_idx().as_tx_ix(), self.cert_idx().as_cert_ix() ) @@ -2175,12 +2193,16 @@ impl HaskellDisplay for SlotNo { trait AsSlotNo { fn as_slot_no(&self) -> String; + fn as_slot_no32(&self) -> String; } impl AsSlotNo for u64 { fn as_slot_no(&self) -> String { format!("SlotNo {self}") } + fn as_slot_no32(&self) -> String { + format!("SlotNo32 {}", self) + } } impl AsSlotNo for SMaybe { @@ -2190,6 +2212,13 @@ impl AsSlotNo for SMaybe { _ => "SNothing".to_string(), } } + + fn as_slot_no32(&self) -> String { + match self { + SMaybe::Some(v) => format!("SJust (SlotNo32 {})", v), + _ => "SNothing".to_string(), + } + } } trait AsBlake2b256 { @@ -2328,7 +2357,7 @@ impl HaskellDisplay for Certificate { } => format!( "RegPool (PoolParams {{ppId = {}, ppVrf = {}, ppPledge = {}, ppCost = {}, ppMargin = {}, ppRewardAccount = {}, ppOwners = {}, ppRelays = {}, ppMetadata = {}}})", operator.as_key_hash(), - vrf_keyhash.to_string().to_haskell_str(), + vrf_keyhash.as_vrf_key_hash(), pledge.as_display_coin(), cost.as_display_coin(), margin.to_haskell_str(), @@ -2483,7 +2512,7 @@ where impl HaskellDisplay for DeltaCoin { fn to_haskell_str(&self) -> String { - format!("DeltaCoin {}", self.0.to_haskell_str()) + format!("DeltaCoin {}", self.0.to_haskell_str_p()) } } @@ -2525,13 +2554,14 @@ impl AsAddress for Bytes { .to_haskell_str() } } -trait AsAuxDataHash { - fn as_aux_data_hash(&self) -> String; + +trait AsTxAuxDataHash { + fn as_tx_aux_data_hash(&self) -> String; } -impl AsAuxDataHash for Bytes { - fn as_aux_data_hash(&self) -> String { - format!("AuxiliaryDataHash {{unsafeAuxiliaryDataHash = SafeHash \"{self}\"}}") +impl AsTxAuxDataHash for Bytes { + fn as_tx_aux_data_hash(&self) -> String { + format!("TxAuxDataHash {{unTxAuxDataHash = SafeHash \"{self}\"}}") } } diff --git a/pallas-network/src/miniprotocols/localtxsubmission/codec.rs b/pallas-network/src/miniprotocols/localtxsubmission/codec.rs index b093df4be..de97b5f67 100644 --- a/pallas-network/src/miniprotocols/localtxsubmission/codec.rs +++ b/pallas-network/src/miniprotocols/localtxsubmission/codec.rs @@ -2501,7 +2501,6 @@ mod tests { buffer.drain(0..pos); Ok(Some(msg)) } - Err(err) if err.is_end_of_input() => Ok(None), Err(err) => Err(Error::Decoding(err.to_string())), } } From 3270b96b2df9919136acf1b525325d9611e59ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Wed, 28 May 2025 20:23:20 +0300 Subject: [PATCH 08/13] Linting --- .../localstate/queries_v16/codec.rs | 1 - .../localstate/queries_v16/mod.rs | 22 ++++++++++++------- .../localtxsubmission/primitives.rs | 4 ++-- .../localtxsubmission/protocol.rs | 7 +++--- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs b/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs index ac2092c07..4379358d9 100644 --- a/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs +++ b/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs @@ -795,7 +795,6 @@ impl<'b, C> minicbor::Decode<'b, C> for CostModels { } } - impl minicbor::Encode for FieldedRewardAccount { fn encode( &self, diff --git a/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs b/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs index edd53b71c..8c71089ce 100644 --- a/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs +++ b/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs @@ -18,7 +18,7 @@ pub mod primitives; pub use primitives::{PoolMetadata, Relay}; use crate::miniprotocols::localtxsubmission::primitives::{ - CommitteeColdCredential, CommitteeHotCredential, ScriptRef, StakeCredential + CommitteeColdCredential, CommitteeHotCredential, ScriptRef, StakeCredential, }; use crate::miniprotocols::Point; @@ -625,13 +625,14 @@ pub enum Vote { Abstain, } -#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)] -pub struct FieldedRewardAccount{ - pub network: Network, pub stake_credential: StakeCredential} +#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord)] +pub struct FieldedRewardAccount { + pub network: Network, + pub stake_credential: StakeCredential, +} impl From<&[u8]> for FieldedRewardAccount { fn from(bytes: &[u8]) -> Self { - let network = if bytes[0] & 0b00000001 != 0 { Network::Mainnet } else { @@ -646,8 +647,10 @@ impl From<&[u8]> for FieldedRewardAccount { StakeCredential::AddrKeyhash(hash.into()) }; - - FieldedRewardAccount{network, stake_credential} + FieldedRewardAccount { + network, + stake_credential, + } } } @@ -659,7 +662,10 @@ pub type ScriptHash = Hash<28>; pub enum GovAction { ParameterChange(Option, PParamsUpdate, Option), HardForkInitiation(Option, ProtocolVersion), - TreasuryWithdrawals(KeyValuePairs, Option), + TreasuryWithdrawals( + KeyValuePairs, + Option, + ), NoConfidence(Option), UpdateCommittee( Option, diff --git a/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs b/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs index ff5264892..0f0b3efe0 100644 --- a/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs +++ b/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs @@ -1,8 +1,8 @@ // Material brought from `pallas-primitives` // TODO: Refactor in order to avoid repetition. use crate::miniprotocols::localstate::queries_v16::{ - Anchor, AssetName, Coin, DRep, Epoch, PolicyId, PoolMetadata, Relay, FieldedRewardAccount, ScriptHash, - UnitInterval, + Anchor, AssetName, Coin, DRep, Epoch, FieldedRewardAccount, PolicyId, PoolMetadata, Relay, + ScriptHash, UnitInterval, }; pub use pallas_codec::utils::KeyValuePairs; pub use pallas_crypto::hash::Hash; diff --git a/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs b/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs index d63e2321b..84e4b49f3 100644 --- a/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs +++ b/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs @@ -2,8 +2,9 @@ use thiserror::Error; use super::primitives::{Certificate, Credential, Language, StakeCredential, Voter}; use crate::miniprotocols::localstate::queries_v16::{ - Anchor, BigInt, FieldedRewardAccount, GovAction, GovActionId, PolicyId, ProposalProcedure, ProtocolVersion, ScriptHash, - TransactionInput, TransactionOutput, Value, Vote, }; + Anchor, BigInt, FieldedRewardAccount, GovAction, GovActionId, PolicyId, ProposalProcedure, + ProtocolVersion, ScriptHash, TransactionInput, TransactionOutput, Value, Vote, +}; pub use crate::miniprotocols::localstate::queries_v16::{Coin, ExUnits, TaggedSet}; use crate::multiplexer; use pallas_codec::minicbor::{self, Decode, Encode}; @@ -461,7 +462,7 @@ pub enum ConwayLedgerFailure { #[cbor(flat)] pub enum ConwayCertsPredFailure { #[n(0)] - WithdrawalsNotInRewardsCERTS(#[n(0)] OHashMap), + WithdrawalsNotInRewardsCERTS(#[n(0)] OHashMap), #[n(1)] CertFailure(#[n(0)] ConwayCertPredFailure), } From e11901a0be97870f9b519b0c56eb293116bb40e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Mon, 1 Sep 2025 13:29:05 +0300 Subject: [PATCH 09/13] Improve Conway::Tx cbor decoding --- pallas-codec/src/utils.rs | 4 ++-- pallas-primitives/src/conway/model.rs | 25 ++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/pallas-codec/src/utils.rs b/pallas-codec/src/utils.rs index cb2eec8c1..f2a944d58 100644 --- a/pallas-codec/src/utils.rs +++ b/pallas-codec/src/utils.rs @@ -1340,11 +1340,11 @@ where fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { match d.datatype()? { minicbor::data::Type::Null => { - d.null()?; + d.skip()?; Ok(Self::Null) } minicbor::data::Type::Undefined => { - d.undefined()?; + d.skip()?; Ok(Self::Undefined) } _ => { diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index d49d8d980..b4bada64d 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -708,7 +708,7 @@ pub struct Block<'b> { #[deprecated(since = "1.0.0-alpha", note = "use `Block` instead")] pub type MintedBlock<'b> = Block<'b>; -#[derive(Clone, Serialize, Deserialize, Encode, Decode, Debug, PartialEq)] +#[derive(Clone, Serialize, Deserialize, Encode, Debug, PartialEq)] pub struct Tx<'b> { #[b(0)] pub transaction_body: KeepRaw<'b, TransactionBody<'b>>, @@ -725,6 +725,29 @@ pub struct Tx<'b> { impl Eq for Tx<'_> {} +impl<'b, C> minicbor::Decode<'b, C> for Tx<'b> +where + KeepRaw<'b, TransactionBody<'b>>: minicbor::Decode<'b, C>, + KeepRaw<'b, WitnessSet<'b>>: minicbor::Decode<'b, C>, + Nullable>: minicbor::Decode<'b, C>, +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + + let transaction_body = d.decode_with(ctx)?; + let transaction_witness_set = d.decode_with(ctx)?; + let success = d.decode_with(ctx).unwrap_or(true); + let auxiliary_data = d.decode_with(ctx).unwrap_or(Nullable::Null); + + Ok(Self { + transaction_body, + transaction_witness_set, + success, + auxiliary_data, + }) + } +} + #[deprecated(since = "1.0.0-alpha", note = "use `Tx` instead")] pub type MintedTx<'b> = Tx<'b>; From e8681d391fbaf12c728b462c0f06baeacd80ba49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Tue, 3 Mar 2026 20:29:02 +0300 Subject: [PATCH 10/13] Add new error variants and PlutusV4 support for Cardano node upgrade to 10.6.2 --- pallas-hardano/src/display/haskell_display.rs | 109 +++++++++++++++--- .../localstate/queries_v16/codec.rs | 3 + .../localstate/queries_v16/mod.rs | 3 + .../miniprotocols/localtxsubmission/codec.rs | 34 ++++++ .../localtxsubmission/primitives.rs | 2 + .../localtxsubmission/protocol.rs | 30 ++++- pallas-primitives/src/conway/model.rs | 6 + pallas-validate/tests/conway.rs | 3 + 8 files changed, 170 insertions(+), 20 deletions(-) diff --git a/pallas-hardano/src/display/haskell_display.rs b/pallas-hardano/src/display/haskell_display.rs index ea86658cc..cd07ecae1 100644 --- a/pallas-hardano/src/display/haskell_display.rs +++ b/pallas-hardano/src/display/haskell_display.rs @@ -39,8 +39,9 @@ use pallas_network::miniprotocols::localtxsubmission::{ ConwayDelegPredFailure, ConwayGovCertPredFailure, ConwayGovPredFailure, ConwayLedgerFailure, ConwayTxCert, ConwayUtxoWPredFailure, DeltaCoin, DisplayAddress, DisplayCoin, DisplayOSet, DisplayPolicyId, DisplayScriptHash, DisplayVotingProcedures, EpochNo, FailureDescription, - KeyHash, Mismatch, OHashMap, PlutusPurpose, SMaybe, SafeHash, ShelleyPoolPredFailure, SlotNo, - TagMismatchDescription, TxOutSource, Utxo, UtxoFailure, UtxosFailure, VKey, ValidityInterval, + KeyHash, Mismatch, MismatchArr, OHashMap, PlutusPurpose, SMaybe, SafeHash, + ShelleyPoolPredFailure, SlotNo, TagMismatchDescription, TxOutSource, Utxo, UtxoFailure, + UtxosFailure, VKey, ValidityInterval, }; use super::{ @@ -80,7 +81,12 @@ impl HaskellDisplay for ConwayLedgerFailure { format!("ConwayTxRefScriptsSizeTooBig ({})", as_mismatch(s2, s1)) } MempoolFailure(e) => format!("ConwayMempoolFailure {}", e.to_haskell_str()), - U8(v) => format!("U8 {v}"), + WithdrawalsMissingAccounts(w) => { + format!("ConwayWithdrawalsMissingAccounts ({})", w.to_haskell_str()) + } + IncompleteWithdrawals(w) => { + format!("ConwayIncompleteWithdrawals ({})", w.to_haskell_str()) + } } } } @@ -132,7 +138,7 @@ impl HaskellDisplay for ConwayCertsPredFailure { match self { WithdrawalsNotInRewardsCERTS(m) => { - format!("WithdrawalsNotInRewardsCERTS {}", m.to_haskell_str_p()) + format!("WithdrawalsNotInRewardsCERTS ({})", m.as_withdrawals()) } CertFailure(e) => format!("CertFailure {}", e.to_haskell_str_p()), } @@ -170,6 +176,13 @@ impl HaskellDisplay for ShelleyPoolPredFailure { size.to_haskell_str_p() ) } + VRFKeyHashAlreadyRegistered(kh, vrf_hash) => { + format!( + "VRFKeyHashAlreadyRegistered {} {}", + kh.to_haskell_str_p(), + vrf_hash.to_haskell_str_p() + ) + } } } } @@ -232,6 +245,14 @@ impl HaskellDisplay for ConwayUtxoWPredFailure { MalformedReferenceScripts(set) => { format!("(MalformedReferenceScripts {})", set.to_haskell_str_p()) } + ScriptIntegrityHashMismatch(m, extra) => { + format!( + "(ScriptIntegrityHashMismatch (Mismatch {{mismatchSupplied = {}, mismatchExpected = {}}}) {})", + m.0.to_haskell_str(), + m.1.to_haskell_str(), + extra.to_haskell_str_p() + ) + } } } } @@ -316,6 +337,9 @@ impl HaskellDisplay for ConwayGovPredFailure { s.to_haskell_str_p() ) } + UnelectedCommitteeVoters(s) => { + format!("UnelectedCommitteeVoters {}", s.to_haskell_str_p()) + } } } } @@ -459,6 +483,10 @@ impl HaskellDisplay for ConwayContextError { "TreasuryDonationFieldNotSupported ({})", display_coin.to_haskell_str() ), + ReferenceInputsNotDisjointFromInputs(inputs) => format!( + "ReferenceInputsNotDisjointFromInputs ({})", + inputs.to_haskell_str() + ), } .to_string() } @@ -534,6 +562,7 @@ impl HaskellDisplay for Language { PlutusV1 => "PlutusV1".to_string(), PlutusV2 => "PlutusV2".to_string(), PlutusV3 => "PlutusV3".to_string(), + PlutusV4 => "PlutusV4".to_string(), } } } @@ -594,6 +623,12 @@ impl HaskellDisplay for ConwayDelegPredFailure { "DelegateeStakePoolNotRegisteredDELEG ({})", hash.to_haskell_str() ), + DepositIncorrectDELEG(m) => { + format!("DepositIncorrectDELEG ({})", as_mismatch(&m.1, &m.0)) + } + RefundIncorrectDELEG(m) => { + format!("RefundIncorrectDELEG ({})", as_mismatch(&m.1, &m.0)) + } } } } @@ -617,6 +652,15 @@ where } } +impl HaskellDisplay for MismatchArr +where + T: HaskellDisplay, +{ + fn to_haskell_str(&self) -> String { + as_mismatch(&self.1, &self.0) + } +} + pub fn as_mismatch(expected: &T, supplied: &T) -> String where T: HaskellDisplay, @@ -761,6 +805,7 @@ fn is_primitive() -> bool { || std::any::TypeId::of::() == std::any::TypeId::of::() || std::any::TypeId::of::() == std::any::TypeId::of::() || std::any::TypeId::of::() == std::any::TypeId::of::() + || std::any::TypeId::of::() == std::any::TypeId::of::() } impl HaskellDisplay for DRep { fn to_haskell_str(&self) -> String { @@ -868,7 +913,7 @@ impl HaskellDisplay for PParamsUpdate { self.max_transaction_size.to_haskell_str(), self.max_block_header_size.to_haskell_str(), self.key_deposit.as_display_coin(), - self.pool_deposit.as_display_coin(), + self.pool_deposit.as_compact_coin(), self.maximum_epoch.as_epoch_interval(), self.desired_number_of_stake_pools.to_haskell_str(), self.pool_pledge_influence.to_haskell_str_p(), @@ -890,7 +935,7 @@ impl HaskellDisplay for PParamsUpdate { self.committee_term_limit.as_epoch_interval(), self.governance_action_validity_period.as_epoch_interval(), self.governance_action_deposit.as_display_coin(), - self.drep_deposit.as_display_coin(), + self.drep_deposit.as_compact_coin(), self.drep_inactivity_period.as_epoch_interval(), self.minfee_refscript_cost_per_byte.to_haskell_str_p() ) @@ -1502,6 +1547,41 @@ impl AsDisplayCoin for Coin { format!("Coin {}", self.to_haskell_str()) } } +trait AsWithdrawals { + fn as_withdrawals(&self) -> String; +} + +impl AsWithdrawals for OHashMap { + fn as_withdrawals(&self) -> String { + format!("Withdrawals {{unWithdrawals = {}}}", self.to_haskell_str()) + } +} + +trait AsCompactCoin { + fn as_compact_coin(&self) -> String; +} + +impl AsCompactCoin for Option { + fn as_compact_coin(&self) -> String { + match self { + Option::Some(v) => format!("SJust (CompactCoin {{unCompactCoin = {}}})", v), + _ => "SNothing".to_string(), + } + } +} + +impl AsCompactCoin for Option { + fn as_compact_coin(&self) -> String { + match self { + Option::Some(v) => format!( + "SJust (CompactCoin {{unCompactCoin = {}}})", + v.to_haskell_str() + ), + _ => "SNothing".to_string(), + } + } +} + trait AsEpochInterval { fn as_epoch_interval(&self) -> String; } @@ -1790,7 +1870,7 @@ impl HaskellDisplay for PseudoScript { fn to_haskell_str(&self) -> String { use PseudoScript::*; match self { - NativeScript(ns) => format!("TimelockScript {}", ns.to_haskell_str()), + NativeScript(ns) => format!("NativeScript {}", ns.to_haskell_str()), PlutusV1Script(ps) => format!( "PlutusScript PlutusV1 {}", Hasher::<224>::hash_tagged(ps.0.as_slice(), 1).as_script_hash() @@ -1813,17 +1893,17 @@ impl HaskellDisplay for NativeScript { let str = match self { ScriptPubkey(key_hash) => { - format!("Signature ({})", key_hash.as_key_hash()) + format!("TimelockSignature ({})", key_hash.as_key_hash()) } - ScriptAll(vec) => format!("AllOf ({})", vec.as_strict_seq()), - ScriptAny(vec) => format!("AnyOf ({})", vec.as_strict_seq()), - ScriptNOfK(m, vec) => format!("MOfN {} ({})", m, vec.as_strict_seq()), - InvalidBefore(slot_no) => format!("TimeStart ({})", slot_no.as_slot_no()), - InvalidHereafter(slot_no) => format!("TimeExpire ({})", slot_no.as_slot_no()), + ScriptAll(vec) => format!("TimelockAllOf ({})", vec.as_strict_seq()), + ScriptAny(vec) => format!("TimelockAnyOf ({})", vec.as_strict_seq()), + ScriptNOfK(m, vec) => format!("TimelockMOf {} ({})", m, vec.as_strict_seq()), + InvalidBefore(slot_no) => format!("TimelockTimeStart ({})", slot_no.as_slot_no()), + InvalidHereafter(slot_no) => format!("TimelockTimeExpire ({})", slot_no.as_slot_no()), }; format!( - "TimelockConstr {} ({})", + "MkTimelock {} ({})", str, Hasher::<256>::hash_cbor(self).as_blake2b256() ) @@ -2693,6 +2773,7 @@ impl HaskellDisplay for CostModels { display_cost_model(1, &self.plutus_v1), display_cost_model(2, &self.plutus_v2), display_cost_model(3, &self.plutus_v3), + display_cost_model(4, &self.plutus_v4), ] .into_iter() .flatten() diff --git a/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs b/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs index 4379358d9..3e8279751 100644 --- a/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs +++ b/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs @@ -775,6 +775,7 @@ impl<'b, C> minicbor::Decode<'b, C> for CostModels { let mut plutus_v1 = None; let mut plutus_v2 = None; let mut plutus_v3 = None; + let mut plutus_v4 = None; let mut unknown: Vec<(u64, CostModel)> = Vec::new(); for (k, v) in models.iter() { @@ -782,6 +783,7 @@ impl<'b, C> minicbor::Decode<'b, C> for CostModels { 0 => plutus_v1 = Some(v.clone()), 1 => plutus_v2 = Some(v.clone()), 2 => plutus_v3 = Some(v.clone()), + 3 => plutus_v4 = Some(v.clone()), _ => unknown.push((*k, v.clone())), } } @@ -790,6 +792,7 @@ impl<'b, C> minicbor::Decode<'b, C> for CostModels { plutus_v1, plutus_v2, plutus_v3, + plutus_v4, unknown: unknown.into(), }) } diff --git a/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs b/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs index 8c71089ce..3b842a6bf 100644 --- a/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs +++ b/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs @@ -324,6 +324,9 @@ pub struct CostModels { #[n(2)] pub plutus_v3: Option, + #[n(3)] + pub plutus_v4: Option, + #[cbor(skip)] pub unknown: KeyValuePairs, } diff --git a/pallas-network/src/miniprotocols/localtxsubmission/codec.rs b/pallas-network/src/miniprotocols/localtxsubmission/codec.rs index de97b5f67..0a9486896 100644 --- a/pallas-network/src/miniprotocols/localtxsubmission/codec.rs +++ b/pallas-network/src/miniprotocols/localtxsubmission/codec.rs @@ -291,6 +291,10 @@ impl<'b, C> Decode<'b, C> for ShelleyPoolPredFailure { d.decode_with(ctx)?, d.decode_with(ctx)?, )), + 6 => Ok(VRFKeyHashAlreadyRegistered( + d.decode_with(ctx)?, + d.decode_with(ctx)?, + )), _ => Err(decode::Error::message(format!( "unknown error tag while decoding ShelleyPoolPredFailure: {tag}" ))), @@ -328,6 +332,36 @@ where } } +/// Mismatch encoded as a CBOR array `[supplied, expected]` (Haskell wraps the +/// pair in an enclosing array for certain variants). +impl<'b, T, C> Decode<'b, C> for super::MismatchArr +where + T: Decode<'b, C>, +{ + fn decode(d: &mut Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + let t0 = d.decode_with(ctx)?; + let t1 = d.decode_with(ctx)?; + Ok(super::MismatchArr(t0, t1)) + } +} + +impl Encode for super::MismatchArr +where + T: Encode, +{ + fn encode( + &self, + e: &mut Encoder, + ctx: &mut C, + ) -> Result<(), encode::Error> { + e.array(2)?; + e.encode_with(&self.0, ctx)?; + e.encode_with(&self.1, ctx)?; + Ok(()) + } +} + impl<'b, C, K: pallas_codec::minicbor::Decode<'b, C>, V: pallas_codec::minicbor::Decode<'b, C>> Decode<'b, C> for OHashMap { diff --git a/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs b/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs index 0f0b3efe0..f916eb783 100644 --- a/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs +++ b/pallas-network/src/miniprotocols/localtxsubmission/primitives.rs @@ -103,6 +103,8 @@ pub enum Language { PlutusV2, #[n(2)] PlutusV3, + #[n(3)] + PlutusV4, } #[derive(Debug, PartialEq, Eq, Clone)] diff --git a/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs b/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs index 84e4b49f3..a74b45d15 100644 --- a/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs +++ b/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs @@ -176,6 +176,8 @@ pub enum ConwayContextError { ProposalProceduresFieldNotSupported(#[n(0)] DisplayOSet), #[n(14)] TreasuryDonationFieldNotSupported(#[n(0)] DisplayCoin), + #[n(15)] + ReferenceInputsNotDisjointFromInputs(#[n(0)] Vec), } #[derive(Debug, Decode, Encode, Clone, Eq, PartialEq)] #[cbor(transparent)] @@ -409,6 +411,8 @@ pub enum ConwayUtxoWPredFailure { MalformedScriptWitnesses(#[n(0)] Set), #[n(17)] MalformedReferenceScripts(#[n(0)] Set), + #[n(18)] + ScriptIntegrityHashMismatch(#[n(0)] MismatchArr>, #[n(1)] SMaybe), } #[derive(Debug, Decode, Encode, Hash, PartialEq, Eq, Clone)] @@ -428,14 +432,17 @@ pub struct DisplayPolicyId(#[n(0)] pub PolicyId); #[derive(Debug, Clone, Eq, PartialEq)] pub struct Mismatch(pub T, pub T); +/// Like `Mismatch` but encoded as a CBOR array `[supplied, expected]` +/// (Haskell wraps the Mismatch pair in an enclosing array for some variants). +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct MismatchArr(pub T, pub T); + #[derive(Debug, Decode, Encode, Clone, Eq, PartialEq)] #[cbor(transparent)] pub struct EpochNo(#[n(0)] pub u64); /// Conway era ledger transaction errors, corresponding to [`ConwayLedgerPredFailure`](https://github.com/IntersectMBO/cardano-ledger/blob/d30a7ae828e802e98277c82e278e570955afc273/eras/conway/impl/src/Cardano/Ledger/Conway/Rules/Ledger.hs#L138-L153) /// in the Haskell sources. -/// -/// The `u8` variant appears for backward compatibility. #[allow(clippy::large_enum_variant)] #[derive(Debug, Decode, Encode, Clone, Eq, PartialEq)] #[cbor(flat)] @@ -455,7 +462,9 @@ pub enum ConwayLedgerFailure { #[n(7)] MempoolFailure(#[n(0)] String), #[n(8)] - U8(#[n(0)] u8), + WithdrawalsMissingAccounts(#[n(0)] OHashMap), + #[n(9)] + IncompleteWithdrawals(#[n(0)] OHashMap>), } // https://github.com/IntersectMBO/cardano-ledger/blob/33e90ea03447b44a389985ca2b158568e5f4ad65/eras/conway/impl/src/Cardano/Ledger/Conway/Rules/Certs.hs#L113 #[derive(Debug, Decode, Encode, Clone, Eq, PartialEq)] @@ -480,6 +489,7 @@ pub enum ConwayCertPredFailure { } // Reminder, encoding of this enum should be custom, see decoder for info. +// Haskell tags: 0, 1, (skip 2 — removed WrongCertificateTypePOOL), 3, 4, 5, 6 #[derive(Debug, Encode, Clone, Eq, PartialEq)] #[cbor(flat)] pub enum ShelleyPoolPredFailure { @@ -487,12 +497,14 @@ pub enum ShelleyPoolPredFailure { StakePoolNotRegisteredOnKeyPOOL(#[n(0)] KeyHash), #[n(1)] StakePoolRetirementWrongEpochPOOL(#[n(0)] Mismatch, #[n(1)] Mismatch), - #[n(2)] - StakePoolCostTooLowPOOL(#[n(0)] Mismatch), #[n(3)] - WrongNetworkPOOL(#[n(0)] Mismatch, #[n(1)] KeyHash), + StakePoolCostTooLowPOOL(#[n(0)] Mismatch), #[n(4)] + WrongNetworkPOOL(#[n(0)] Mismatch, #[n(1)] KeyHash), + #[n(5)] PoolMedataHashTooBig(#[n(0)] KeyHash, #[n(1)] i64), + #[n(6)] + VRFKeyHashAlreadyRegistered(#[n(0)] KeyHash, #[n(1)] Bytes), } // https://github.com/IntersectMBO/cardano-ledger/blob/33e90ea03447b44a389985ca2b158568e5f4ad65/eras/conway/impl/src/Cardano/Ledger/Conway/Rules/GovCert.hs#L118C6-L118C30 @@ -529,6 +541,10 @@ pub enum ConwayDelegPredFailure { DelegateeDRepNotRegisteredDELEG(#[n(0)] Credential), #[n(6)] DelegateeStakePoolNotRegisteredDELEG(#[n(0)] KeyHash), + #[n(7)] + DepositIncorrectDELEG(#[n(0)] MismatchArr), + #[n(8)] + RefundIncorrectDELEG(#[n(0)] MismatchArr), } // https://github.com/IntersectMBO/cardano-ledger/blob/33e90ea03447b44a389985ca2b158568e5f4ad65/eras/conway/impl/src/Cardano/Ledger/Conway/Rules/Gov.hs#L164 @@ -578,6 +594,8 @@ pub enum ConwayGovPredFailure { ProposalReturnAccountDoesNotExist(#[n(0)] FieldedRewardAccount), #[n(17)] TreasuryWithdrawalReturnAccountsDoNotExist(#[n(0)] Vec), + #[n(18)] + UnelectedCommitteeVoters(#[n(0)] Vec), } /// Reject reason. It can be a pair of an era number and a sequence of errors, diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index b4bada64d..0890765d7 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -166,6 +166,9 @@ pub struct CostModels { #[n(2)] pub plutus_v3: Option, + #[n(3)] + pub plutus_v4: Option, + #[cbor(skip)] pub unknown: BTreeMap, } @@ -177,6 +180,7 @@ impl<'b, C> minicbor::Decode<'b, C> for CostModels { let mut plutus_v1 = None; let mut plutus_v2 = None; let mut plutus_v3 = None; + let mut plutus_v4 = None; let mut unknown: Vec<(u64, CostModel)> = Vec::new(); for (k, v) in models.iter() { @@ -184,6 +188,7 @@ impl<'b, C> minicbor::Decode<'b, C> for CostModels { 0 => plutus_v1 = Some(v.clone()), 1 => plutus_v2 = Some(v.clone()), 2 => plutus_v3 = Some(v.clone()), + 3 => plutus_v4 = Some(v.clone()), _ => unknown.push((*k, v.clone())), } } @@ -192,6 +197,7 @@ impl<'b, C> minicbor::Decode<'b, C> for CostModels { plutus_v1, plutus_v2, plutus_v3, + plutus_v4, unknown: unknown.into_iter().collect(), }) } diff --git a/pallas-validate/tests/conway.rs b/pallas-validate/tests/conway.rs index 26c0ec4c8..1ffc3cd0d 100644 --- a/pallas-validate/tests/conway.rs +++ b/pallas-validate/tests/conway.rs @@ -1187,6 +1187,7 @@ mod conway_tests { plutus_v2: None, plutus_v3: None, + plutus_v4: None, unknown: BTreeMap::default(), }, execution_costs: pallas_primitives::ExUnitPrices { @@ -1532,6 +1533,7 @@ mod conway_tests { 281145, 18848, 0, 1, 180194, 159, 1, 1, 158519, 8942, 0, 1, 159378, 8813, 0, 1, 107490, 3298, 1, 106057, 655, 1, 1964219, 24520, 3, ]), + plutus_v4: None, unknown: BTreeMap::default(), }, execution_costs: pallas_primitives::ExUnitPrices { @@ -1715,6 +1717,7 @@ mod conway_tests { 281145, 18848, 0, 1, 180194, 159, 1, 1, 158519, 8942, 0, 1, 159378, 8813, 0, 1, 107490, 3298, 1, 106057, 655, 1, 1964219, 24520, 3, ]), + plutus_v4: None, unknown: BTreeMap::default(), }, execution_costs: pallas_primitives::ExUnitPrices { From 529f5d6f258928c097670120c9cf4f5256b6e95a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Mon, 23 Mar 2026 21:32:20 +0300 Subject: [PATCH 11/13] fix: Applied CodeRabbit's PR feedback and more --- pallas-hardano/src/display/haskell_display.rs | 72 +++++++++++-------- pallas-hardano/src/display/haskell_error.rs | 16 ++--- .../localstate/queries_v16/codec.rs | 11 ++- .../localstate/queries_v16/mod.rs | 30 ++++++-- pallas-primitives/src/conway/model.rs | 12 +++- 5 files changed, 93 insertions(+), 48 deletions(-) diff --git a/pallas-hardano/src/display/haskell_display.rs b/pallas-hardano/src/display/haskell_display.rs index cd07ecae1..296fa5d2a 100644 --- a/pallas-hardano/src/display/haskell_display.rs +++ b/pallas-hardano/src/display/haskell_display.rs @@ -393,6 +393,8 @@ impl HaskellDisplay for UtxoFailure { format!("(CollateralContainsNonADA {})", value.to_haskell_str_p()) } NoCollateralInputs => "NoCollateralInputs".to_string(), + // as_mismatch order: (supplied, expected). The transaction supplies + // `actual` collateral inputs, the ledger expects at most `max`. TooManyCollateralInputs(actual, max) => { format!("(TooManyCollateralInputs ({}))", as_mismatch(actual, max)) } @@ -526,12 +528,43 @@ impl HaskellDisplay for BabbageContextError { impl HaskellDisplay for DecoderError { fn to_haskell_str(&self) -> String { match self { - DecoderError::DeserialiseFailure(str, failure) => format!( + DecoderError::DeserialiseFailure(s, failure) => format!( "DecoderErrorDeserialiseFailure {} {}", - str.to_haskell_str(), + s.to_haskell_str(), failure.to_haskell_str_p() ), - _ => format!("DecoderError ({:?})", self), + DecoderError::CanonicityViolation(s) => { + format!("DecoderErrorCanonicityViolation {}", s.to_haskell_str()) + } + DecoderError::Custom(s1, s2) => { + format!( + "DecoderErrorCustom {} {}", + s1.to_haskell_str(), + s2.to_haskell_str() + ) + } + DecoderError::EmptyList(s) => { + format!("DecoderErrorEmptyList {}", s.to_haskell_str()) + } + DecoderError::Leftover(s, bytes) => { + format!( + "DecoderErrorLeftover {} \"{}\"", + s.to_haskell_str(), + hex::encode(bytes) + ) + } + DecoderError::SizeMismatch(s, expected, actual) => { + format!( + "DecoderErrorSizeMismatch {} {} {}", + s.to_haskell_str(), + expected, + actual + ) + } + DecoderError::UnknownTag(s, tag) => { + format!("DecoderErrorUnknownTag {} {}", s.to_haskell_str(), tag) + } + DecoderError::Void => "DecoderErrorVoid".to_string(), } } } @@ -672,17 +705,6 @@ where ) } -pub fn as_mismatch_p(expected: &T, supplied: &T) -> String -where - T: HaskellDisplay, -{ - format!( - "Mismatch {{mismatchSupplied = {}, mismatchExpected = {}}}", - supplied.to_haskell_str_p(), - expected.to_haskell_str_p() - ) -} - impl HaskellDisplay for FieldedRewardAccount { fn to_haskell_str(&self) -> String { format!( @@ -1561,15 +1583,6 @@ trait AsCompactCoin { fn as_compact_coin(&self) -> String; } -impl AsCompactCoin for Option { - fn as_compact_coin(&self) -> String { - match self { - Option::Some(v) => format!("SJust (CompactCoin {{unCompactCoin = {}}})", v), - _ => "SNothing".to_string(), - } - } -} - impl AsCompactCoin for Option { fn as_compact_coin(&self) -> String { match self { @@ -2273,6 +2286,9 @@ impl HaskellDisplay for SlotNo { trait AsSlotNo { fn as_slot_no(&self) -> String; +} + +trait AsSlotNo32 { fn as_slot_no32(&self) -> String; } @@ -2280,6 +2296,9 @@ impl AsSlotNo for u64 { fn as_slot_no(&self) -> String { format!("SlotNo {self}") } +} + +impl AsSlotNo32 for u64 { fn as_slot_no32(&self) -> String { format!("SlotNo32 {}", self) } @@ -2292,13 +2311,6 @@ impl AsSlotNo for SMaybe { _ => "SNothing".to_string(), } } - - fn as_slot_no32(&self) -> String { - match self { - SMaybe::Some(v) => format!("SJust (SlotNo32 {})", v), - _ => "SNothing".to_string(), - } - } } trait AsBlake2b256 { diff --git a/pallas-hardano/src/display/haskell_error.rs b/pallas-hardano/src/display/haskell_error.rs index 62be7d7a5..18eac2ec4 100644 --- a/pallas-hardano/src/display/haskell_error.rs +++ b/pallas-hardano/src/display/haskell_error.rs @@ -13,23 +13,23 @@ pub fn wrap_error_response(error: TxValidationError) -> TxSubmitFail { } /// Generates Haskell 'identical' string for the error response -pub fn as_node_submit_error(error: TxValidationError) -> String { - serde_json::to_string(&wrap_error_response(error)).unwrap() +pub fn as_node_submit_error(error: TxValidationError) -> Result { + serde_json::to_string(&wrap_error_response(error)) } /// Generates Haskell 'similar' string for the error response in case of decode failure /// Only difference will be the provided decode failure message, Rust vs Haskell -pub fn as_cbor_decode_failure(message: String, position: u64) -> String { +pub fn as_cbor_decode_failure(message: String, position: u64) -> Result { let inner_errors = vec![DecoderError::DeserialiseFailure( "Shelley Tx".to_string(), DeserialiseFailure(position, message), )]; let error = TxSubmitFail::TxSubmitFail(TxCmdError::TxReadError(inner_errors)); - serde_json::to_string(&error).unwrap() + serde_json::to_string(&error) } -pub fn serialize_error(error: TxValidationError) -> serde_json::Value { - serde_json::to_value(wrap_error_response(error)).unwrap() +pub fn serialize_error(error: TxValidationError) -> Result { + serde_json::to_value(wrap_error_response(error)) } /// https://github.com/IntersectMBO/cardano-node/blob/9dbf0b141e67ec2dfd677c77c63b1673cf9c5f3e/cardano-submit-api/src/Cardano/TxSubmit/Types.hs#L54 @@ -147,14 +147,14 @@ fn test_submit_api_serialization() { let error = decode_error("81820681820764f0aab883"); assert_eq!("{\"tag\":\"TxSubmitFail\",\"contents\":{\"tag\":\"TxCmdTxSubmitValidationError\",\"contents\":{\"tag\":\"TxValidationErrorInCardanoMode\",\"contents\":{\"kind\":\"ShelleyTxValidationError\",\"error\":[\"ConwayMempoolFailure \\\"\\\\175619\\\"\"],\"era\":\"ShelleyBasedEraConway\"}}}}", - as_node_submit_error(error)); + as_node_submit_error(error).unwrap()); } #[test] #[allow(non_snake_case)] fn test_submit_api_decode_failure() { assert_eq!( "{\"tag\":\"TxSubmitFail\",\"contents\":{\"tag\":\"TxCmdTxReadError\",\"contents\":[\"DecoderErrorDeserialiseFailure \\\"Shelley Tx\\\" (DeserialiseFailure 0 (\\\"expected list len or indef\\\"))\"]}}", - as_cbor_decode_failure("expected list len or indef".to_string(), 0)); + as_cbor_decode_failure("expected list len or indef".to_string(), 0).unwrap()); } #[cfg(test)] diff --git a/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs b/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs index 3e8279751..764d70552 100644 --- a/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs +++ b/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs @@ -804,7 +804,11 @@ impl minicbor::Encode for FieldedRewardAccount { e: &mut minicbor::Encoder, _ctx: &mut C, ) -> Result<(), minicbor::encode::Error> { - let network: u8 = self.network.clone().into(); + let mut prefix: u8 = self.network.clone().into(); + + if matches!(self.stake_credential, StakeCredential::ScriptHash(_)) { + prefix |= 0b0001_0000; + } let hash = match &self.stake_credential { StakeCredential::ScriptHash(hash) | StakeCredential::AddrKeyhash(hash) => hash, @@ -812,7 +816,7 @@ impl minicbor::Encode for FieldedRewardAccount { let mut bytes: [u8; 29] = [0u8; 29]; - bytes[0] = network; + bytes[0] = prefix; bytes[1..].copy_from_slice(hash.as_ref()); e.bytes(&bytes)?; @@ -825,7 +829,8 @@ impl<'b, C> minicbor::Decode<'b, C> for FieldedRewardAccount { d: &mut minicbor::Decoder<'b>, _ctx: &mut C, ) -> Result { - Ok(d.bytes()?.into()) + let bytes = d.bytes()?; + FieldedRewardAccount::try_from(bytes).map_err(minicbor::decode::Error::message) } } diff --git a/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs b/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs index 3b842a6bf..7896b9363 100644 --- a/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs +++ b/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs @@ -2,6 +2,7 @@ use pallas_crypto::hash::Hash; use std::collections::{BTreeMap, BTreeSet}; +use std::fmt; use std::hash::Hash as StdHash; use std::ops::Deref; // required for derive attrs to work @@ -634,8 +635,27 @@ pub struct FieldedRewardAccount { pub stake_credential: StakeCredential, } -impl From<&[u8]> for FieldedRewardAccount { - fn from(bytes: &[u8]) -> Self { +#[derive(Debug)] +pub struct InvalidRewardAccount(pub usize); + +impl fmt::Display for InvalidRewardAccount { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "invalid reward account length: expected 29, got {}", + self.0 + ) + } +} + +impl TryFrom<&[u8]> for FieldedRewardAccount { + type Error = InvalidRewardAccount; + + fn try_from(bytes: &[u8]) -> Result { + if bytes.len() != 29 { + return Err(InvalidRewardAccount(bytes.len())); + } + let network = if bytes[0] & 0b00000001 != 0 { Network::Mainnet } else { @@ -644,16 +664,16 @@ impl From<&[u8]> for FieldedRewardAccount { let mut hash = [0; 28]; hash.copy_from_slice(&bytes[1..29]); - let stake_credential = if &bytes[0] & 0b00010000 != 0 { + let stake_credential = if bytes[0] & 0b00010000 != 0 { StakeCredential::ScriptHash(hash.into()) } else { StakeCredential::AddrKeyhash(hash.into()) }; - FieldedRewardAccount { + Ok(FieldedRewardAccount { network, stake_credential, - } + }) } } diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs index 0890765d7..1ee0bc95d 100644 --- a/pallas-primitives/src/conway/model.rs +++ b/pallas-primitives/src/conway/model.rs @@ -742,8 +742,16 @@ where let transaction_body = d.decode_with(ctx)?; let transaction_witness_set = d.decode_with(ctx)?; - let success = d.decode_with(ctx).unwrap_or(true); - let auxiliary_data = d.decode_with(ctx).unwrap_or(Nullable::Null); + let success = match d.decode_with(ctx) { + Ok(v) => v, + Err(e) if e.is_end_of_input() => true, + Err(e) => return Err(e), + }; + let auxiliary_data = match d.decode_with(ctx) { + Ok(v) => v, + Err(e) if e.is_end_of_input() => Nullable::Null, + Err(e) => return Err(e), + }; Ok(Self { transaction_body, From a03ae024c98d4c03ad724af3f27eddab55b6c1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Wed, 25 Mar 2026 19:20:05 +0300 Subject: [PATCH 12/13] fix: validate reward account header per CIP-19 --- .../localstate/queries_v16/codec.rs | 105 +++++++++++++++++- .../localstate/queries_v16/mod.rs | 34 ++++-- .../localtxsubmission/protocol.rs | 2 +- 3 files changed, 125 insertions(+), 16 deletions(-) diff --git a/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs b/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs index 764d70552..48218beb4 100644 --- a/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs +++ b/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs @@ -804,16 +804,17 @@ impl minicbor::Encode for FieldedRewardAccount { e: &mut minicbor::Encoder, _ctx: &mut C, ) -> Result<(), minicbor::encode::Error> { - let mut prefix: u8 = self.network.clone().into(); + let (hash, is_script) = match &self.stake_credential { + StakeCredential::ScriptHash(h) => (h, true), + StakeCredential::AddrKeyhash(h) => (h, false), + }; - if matches!(self.stake_credential, StakeCredential::ScriptHash(_)) { + // Network yields 0 (testnet) or 1 (mainnet), always fits in the lower nibble. + let mut prefix: u8 = 0b1110_0000 | u8::from(self.network); + if is_script { prefix |= 0b0001_0000; } - let hash = match &self.stake_credential { - StakeCredential::ScriptHash(hash) | StakeCredential::AddrKeyhash(hash) => hash, - }; - let mut bytes: [u8; 29] = [0u8; 29]; bytes[0] = prefix; @@ -916,4 +917,96 @@ pub mod tests { assert_eq!(hex::encode(bytes), hex::encode(bytes2)); } + + #[test] + fn test_fielded_reward_account_header_bytes() { + use super::{FieldedRewardAccount, InvalidRewardAccount, StakeCredential}; + use crate::miniprotocols::localtxsubmission::Network; + use pallas_crypto::hash::Hash; + + let zero_hash: Hash<28> = [0u8; 28].into(); + + // Mainnet + KeyHash → 0xE1 + let acc = FieldedRewardAccount { + network: Network::Mainnet, + stake_credential: StakeCredential::AddrKeyhash(zero_hash), + }; + let encoded = minicbor::to_vec(&acc).unwrap(); + // CBOR bytes tag + 29 bytes payload; first payload byte is the header + let payload = &encoded[2..]; // skip CBOR major type + length + assert_eq!(payload[0], 0xE1); + + // Mainnet + ScriptHash → 0xF1 + let acc = FieldedRewardAccount { + network: Network::Mainnet, + stake_credential: StakeCredential::ScriptHash(zero_hash), + }; + let encoded = minicbor::to_vec(&acc).unwrap(); + let payload = &encoded[2..]; + assert_eq!(payload[0], 0xF1); + + // Testnet + KeyHash → 0xE0 + let acc = FieldedRewardAccount { + network: Network::Testnet, + stake_credential: StakeCredential::AddrKeyhash(zero_hash), + }; + let encoded = minicbor::to_vec(&acc).unwrap(); + let payload = &encoded[2..]; + assert_eq!(payload[0], 0xE0); + + // Testnet + ScriptHash → 0xF0 + let acc = FieldedRewardAccount { + network: Network::Testnet, + stake_credential: StakeCredential::ScriptHash(zero_hash), + }; + let encoded = minicbor::to_vec(&acc).unwrap(); + let payload = &encoded[2..]; + assert_eq!(payload[0], 0xF0); + } + + #[test] + fn test_fielded_reward_account_roundtrip() { + use super::{FieldedRewardAccount, StakeCredential}; + use crate::miniprotocols::localtxsubmission::Network; + use pallas_crypto::hash::Hash; + + let hash: Hash<28> = [0xAB; 28].into(); + + for (network, credential) in [ + (Network::Mainnet, StakeCredential::AddrKeyhash(hash)), + (Network::Mainnet, StakeCredential::ScriptHash(hash)), + (Network::Testnet, StakeCredential::AddrKeyhash(hash)), + (Network::Testnet, StakeCredential::ScriptHash(hash)), + ] { + let original = FieldedRewardAccount { + network, + stake_credential: credential, + }; + let encoded = minicbor::to_vec(&original).unwrap(); + let decoded: FieldedRewardAccount = minicbor::decode(&encoded).unwrap(); + assert_eq!(original, decoded); + } + } + + #[test] + fn test_fielded_reward_account_rejects_invalid_header() { + use super::{FieldedRewardAccount, InvalidRewardAccount}; + + // Old buggy encoding: 0x01 (mainnet, no type nibble) + let mut bad_bytes = [0u8; 29]; + bad_bytes[0] = 0x01; + let err = FieldedRewardAccount::try_from(bad_bytes.as_slice()).unwrap_err(); + assert!(matches!(err, InvalidRewardAccount::InvalidHeaderType(_))); + + // Wrong length + let short = [0xE1; 10]; + let err = FieldedRewardAccount::try_from(short.as_slice()).unwrap_err(); + assert!(matches!(err, InvalidRewardAccount::InvalidLength(10))); + + // Shelley payment address type (0x00) should be rejected + let mut shelley_header = [0u8; 29]; + shelley_header[0] = 0x01; // type 0b0000, mainnet + let err = FieldedRewardAccount::try_from(shelley_header.as_slice()).unwrap_err(); + assert!(matches!(err, InvalidRewardAccount::InvalidHeaderType(_))); + } } diff --git a/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs b/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs index 7896b9363..bc0cfabe3 100644 --- a/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs +++ b/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs @@ -636,27 +636,43 @@ pub struct FieldedRewardAccount { } #[derive(Debug)] -pub struct InvalidRewardAccount(pub usize); +pub enum InvalidRewardAccount { + InvalidLength(usize), + InvalidHeaderType(u8), +} impl fmt::Display for InvalidRewardAccount { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "invalid reward account length: expected 29, got {}", - self.0 - ) + match self { + Self::InvalidLength(len) => { + write!(f, "invalid reward account length: expected 29, got {len}") + } + Self::InvalidHeaderType(header) => { + write!(f, "invalid reward account header type: 0x{header:02x}") + } + } } } +impl std::error::Error for InvalidRewardAccount {} + impl TryFrom<&[u8]> for FieldedRewardAccount { type Error = InvalidRewardAccount; fn try_from(bytes: &[u8]) -> Result { if bytes.len() != 29 { - return Err(InvalidRewardAccount(bytes.len())); + return Err(InvalidRewardAccount::InvalidLength(bytes.len())); + } + + // Header byte layout: [type(4 bits) | network(4 bits)] + // Upper nibble must be 0b1110 (key hash) or 0b1111 (script hash) + // per CIP-19 reward account address format. + let header_type = bytes[0] >> 4; + if !matches!(header_type, 0b1110 | 0b1111) { + return Err(InvalidRewardAccount::InvalidHeaderType(bytes[0])); } - let network = if bytes[0] & 0b00000001 != 0 { + let network = if bytes[0] & 0b0000_0001 != 0 { Network::Mainnet } else { Network::Testnet @@ -664,7 +680,7 @@ impl TryFrom<&[u8]> for FieldedRewardAccount { let mut hash = [0; 28]; hash.copy_from_slice(&bytes[1..29]); - let stake_credential = if bytes[0] & 0b00010000 != 0 { + let stake_credential = if header_type == 0b1111 { StakeCredential::ScriptHash(hash.into()) } else { StakeCredential::AddrKeyhash(hash.into()) diff --git a/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs b/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs index a74b45d15..5979c4bcf 100644 --- a/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs +++ b/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs @@ -265,7 +265,7 @@ pub struct Utxo(pub OHashMap); #[derive(Debug, PartialEq, Eq, Clone)] pub struct OHashMap(pub Vec<(K, V)>); -#[derive(Encode, Decode, Debug, Clone, Eq, PartialEq, PartialOrd, Ord)] +#[derive(Encode, Decode, Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] #[cbor(index_only)] pub enum Network { #[n(0)] From ed422bc0d5a09f77617d4882dd4c71089a8bcdb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Wed, 25 Mar 2026 21:00:00 +0300 Subject: [PATCH 13/13] fix: apply PR feedback --- pallas-hardano/src/display/haskell_error.rs | 23 +++++++++++++------ .../localstate/queries_v16/codec.rs | 2 +- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/pallas-hardano/src/display/haskell_error.rs b/pallas-hardano/src/display/haskell_error.rs index 18eac2ec4..cca4af647 100644 --- a/pallas-hardano/src/display/haskell_error.rs +++ b/pallas-hardano/src/display/haskell_error.rs @@ -7,7 +7,7 @@ use super::haskell_display::HaskellDisplay; /// Mimicks the json data structure of the error response from the cardano-submit-api pub fn wrap_error_response(error: TxValidationError) -> TxSubmitFail { - TxSubmitFail::TxSubmitFail(TxCmdError::TxCmdTxSubmitValidationError( + TxSubmitFail::TxSubmitFail(TxCmdError::SubmitValidationError( TxValidationErrorInCardanoMode::TxValidationErrorInCardanoMode(error), )) } @@ -24,7 +24,7 @@ pub fn as_cbor_decode_failure(message: String, position: u64) -> Result), - TxCmdTxSubmitValidationError(TxValidationErrorInCardanoMode), + #[serde(rename = "TxCmdTxReadError")] + ReadError(Vec), + #[serde(rename = "TxCmdTxSubmitValidationError")] + SubmitValidationError(TxValidationErrorInCardanoMode), } /// https://github.com/IntersectMBO/cardano-api/blob/d7c62a04ebf18d194a6ea70e6765eb7691d57668/cardano-api/internal/Cardano/Api/InMode.hs#L259 @@ -70,7 +72,7 @@ pub struct EraMismatch { } /// https://github.com/IntersectMBO/cardano-base/blob/391a2c5cfd30d2234097e000dbd8d9db21ef94d7/cardano-binary/src/Cardano/Binary/FromCBOR.hs#L90 -#[derive(Debug, Serialize)] +#[derive(Debug)] pub enum DecoderError { CanonicityViolation(String), Custom(String, String), @@ -81,8 +83,15 @@ pub enum DecoderError { UnknownTag(String, u8), Void, } + +impl Serialize for DecoderError { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_str(&self.to_haskell_str()) + } +} + /// https://hackage.haskell.org/package/serialise-0.2.6.1/docs/Codec-Serialise.html#t:DeserialiseFailure -#[derive(Debug, Serialize)] +#[derive(Debug)] pub struct DeserialiseFailure(pub u64, pub String); // diff --git a/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs b/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs index 48218beb4..aadf6f35b 100644 --- a/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs +++ b/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs @@ -920,7 +920,7 @@ pub mod tests { #[test] fn test_fielded_reward_account_header_bytes() { - use super::{FieldedRewardAccount, InvalidRewardAccount, StakeCredential}; + use super::{FieldedRewardAccount, StakeCredential}; use crate::miniprotocols::localtxsubmission::Network; use pallas_crypto::hash::Hash;