Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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),
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);

// 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