Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pallas-codec/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1340,11 +1340,11 @@ where
fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result<Self, minicbor::decode::Error> {
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)
}
_ => {
Expand Down
424 changes: 283 additions & 141 deletions pallas-hardano/src/display/haskell_display.rs

Large diffs are not rendered by default.

69 changes: 55 additions & 14 deletions pallas-hardano/src/display/haskell_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,29 @@ 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),
))
}

/// 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 'identical' string for the error response
pub fn as_node_submit_error(error: TxValidationError) -> Result<String, serde_json::Error> {
serde_json::to_string(&wrap_error_response(error))
}

pub fn serialize_error(error: TxValidationError) -> serde_json::Value {
serde_json::to_value(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) -> Result<String, serde_json::Error> {
let inner_errors = vec![DecoderError::DeserialiseFailure(
"Shelley Tx".to_string(),
DeserialiseFailure(position, message),
)];
let error = TxSubmitFail::TxSubmitFail(TxCmdError::ReadError(inner_errors));
serde_json::to_string(&error)
}

pub fn serialize_error(error: TxValidationError) -> Result<serde_json::Value, serde_json::Error> {
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
Expand All @@ -36,9 +47,12 @@ pub enum TxSubmitFail {
#[derive(Debug, Serialize)]
#[serde(tag = "tag", content = "contents")]
pub enum TxCmdError {
#[serde(rename = "TxCmdSocketEnvError")]
SocketEnvError(String),
Comment thread
ginnun marked this conversation as resolved.
TxReadError(Vec<DecoderError>),
TxCmdTxSubmitValidationError(TxValidationErrorInCardanoMode),
#[serde(rename = "TxCmdTxReadError")]
ReadError(Vec<DecoderError>),
#[serde(rename = "TxCmdTxSubmitValidationError")]
SubmitValidationError(TxValidationErrorInCardanoMode),
}

/// https://github.com/IntersectMBO/cardano-api/blob/d7c62a04ebf18d194a6ea70e6765eb7691d57668/cardano-api/internal/Cardano/Api/InMode.hs#L259
Expand All @@ -57,9 +71,28 @@ 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)]
pub enum DecoderError {
CanonicityViolation(String),
Custom(String, String),
DeserialiseFailure(String, DeserialiseFailure),
EmptyList(String),
Leftover(String, Vec<u8>),
SizeMismatch(String, u64, u64),
UnknownTag(String, u8),
Void,
}

impl Serialize for DecoderError {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
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)]
pub struct DeserialiseFailure(pub u64, pub String);

//
// Haskell JSON serializations
Expand All @@ -83,7 +116,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(
Expand All @@ -108,9 +140,10 @@ enum ShelleyBasedEraJson {
Conway,
}

fn use_haskell_display<S>(fails: &[ConwayLedgerFailure], serializer: S) -> Result<S::Ok, S::Error>
fn use_haskell_display<S, T>(fails: &[T], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: HaskellDisplay,
{
let fails_str = fails.iter().map(|fail| fail.to_haskell_str());
serializer.collect_seq(fails_str)
Expand All @@ -122,7 +155,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).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).unwrap());
}

#[cfg(test)]
Expand Down
132 changes: 132 additions & 0 deletions pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -775,13 +775,15 @@ 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() {
match k {
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())),
}
}
Expand All @@ -790,11 +792,49 @@ impl<'b, C> minicbor::Decode<'b, C> for CostModels {
plutus_v1,
plutus_v2,
plutus_v3,
plutus_v4,
unknown: unknown.into(),
})
}
}

impl<C> minicbor::Encode<C> for FieldedRewardAccount {
fn encode<W: minicbor::encode::Write>(
&self,
e: &mut minicbor::Encoder<W>,
_ctx: &mut C,
) -> Result<(), minicbor::encode::Error<W::Error>> {
let (hash, is_script) = match &self.stake_credential {
StakeCredential::ScriptHash(h) => (h, true),
StakeCredential::AddrKeyhash(h) => (h, false),
};

// 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 mut bytes: [u8; 29] = [0u8; 29];

bytes[0] = prefix;
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<Self, minicbor::decode::Error> {
let bytes = d.bytes()?;
FieldedRewardAccount::try_from(bytes).map_err(minicbor::decode::Error::message)
}
}

#[cfg(test)]
pub mod tests {
use pallas_codec::minicbor;
Expand Down Expand Up @@ -877,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, 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);
Comment on lines +1051 to +1054

// 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(_)));
}
}
Loading
Loading