From 57b36d3a7c840569d4d86f6d805a42aa4cf85347 Mon Sep 17 00:00:00 2001 From: Cobb Date: Mon, 4 May 2026 12:04:11 -0700 Subject: [PATCH] =?UTF-8?q?pallas-txbuilder:=20thread=20auxiliary=5Fdata?= =?UTF-8?q?=20through=20StagingTransaction=20=E2=86=92=20Conway=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit addresses the four TODO comments in pallas-txbuilder/src/transaction/model.rs and conway.rs around auxiliary_data + auxiliary_data_hash. without this the conway builder hardcodes auxiliary_data: None, which blocks CIP-20 / CIP-25 / CIP-68 metadata. implementation: - new StagingTransaction field: pub auxiliary_data: Option>. stored as opaque cbor bytes (caller encodes alonzo::AuxiliaryData) since AuxiliaryData itself doesn't impl Eq, which the rest of StagingTransaction requires. - builder methods .auxiliary_data(cbor) and .clear_auxiliary_data(). - conway::build_conway_raw now decodes the bytes back into AuxiliaryData::decode_fragment and plugs it into pallas_tx.auxiliary_data. the existing auxiliary_data_hash population block is unchanged — it already computes the hash from pallas_tx.auxiliary_data after assignment. - decode failures map to TxBuilderError::CorruptedTxBytes. tests: - auxiliary_data_round_trips_through_build: encodes a CIP-25-shaped metadata, attaches, builds, decodes resulting tx, asserts both body.auxiliary_data_hash matches expected_hash.compute_hash() and the aux re-encodes byte-equivalent. - no_auxiliary_data_means_no_hash: confirms the absence path still works (no aux → hash field stays None). both pre-existing tests (staging_json_roundtrip, built_json_roundtrip, test_script_data_hash) continue to pass — the new field defaults to None and rides alongside. PR upstream pending; using as a vendored patch via [patch.crates-io] on the Sulkta side until merge. --- pallas-txbuilder/src/conway.rs | 85 ++++++++++++++++++- pallas-txbuilder/src/transaction/model.rs | 26 +++++- pallas-txbuilder/src/transaction/serialise.rs | 1 + 3 files changed, 107 insertions(+), 5 deletions(-) diff --git a/pallas-txbuilder/src/conway.rs b/pallas-txbuilder/src/conway.rs index a958078..6bbef86 100644 --- a/pallas-txbuilder/src/conway.rs +++ b/pallas-txbuilder/src/conway.rs @@ -4,8 +4,8 @@ use pallas_codec::utils::CborWrap; use pallas_crypto::hash::Hash; use pallas_primitives::{ conway::{ - DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, NonZeroInt, PlutusData, - PlutusScript, PostAlonzoTransactionOutput, PseudoScript as PallasScript, + AuxiliaryData, DatumOption, ExUnits as PallasExUnits, NativeScript, NetworkId, NonZeroInt, + PlutusData, PlutusScript, PostAlonzoTransactionOutput, PseudoScript as PallasScript, PseudoTransactionOutput, Redeemer, RedeemerTag, TransactionBody, TransactionInput, Tx, Value, WitnessSet, }, @@ -270,8 +270,14 @@ impl BuildConway for StagingTransaction { Some(witness_set_redeemers) }, }, - success: true, // TODO - auxiliary_data: None.into(), // TODO + success: true, // TODO + auxiliary_data: self + .auxiliary_data + .as_deref() + .map(AuxiliaryData::decode_fragment) + .transpose() + .map_err(|_| TxBuilderError::CorruptedTxBytes)? + .into(), }; // TODO: pallas auxiliary_data_hash should be Hash<32> not Bytes @@ -375,3 +381,74 @@ impl Output { )) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::transaction::model::StagingTransaction; + use pallas_codec::minicbor; + use pallas_primitives::alonzo::{AuxiliaryData, Metadatum, PostAlonzoAuxiliaryData}; + use pallas_traverse::ComputeHash; + + /// Encode a CIP-25-shaped auxiliary data, attach it to a tx, and + /// confirm the built tx body carries the matching auxiliary_data_hash. + /// Round-trip protection for the fork's metadata plumbing. + #[test] + fn auxiliary_data_round_trips_through_build() { + // Minimal CIP-25-style metadata: label 721 → text "test". + let metadata: pallas_primitives::alonzo::Metadata = vec![ + (721u64, Metadatum::Text("test".into())), + ] + .into(); + let aux = AuxiliaryData::PostAlonzo(PostAlonzoAuxiliaryData { + metadata: Some(metadata), + native_scripts: None, + plutus_scripts: None, + }); + let aux_bytes = minicbor::to_vec(&aux).expect("encode aux"); + let expected_hash = aux.compute_hash(); + + let tx = StagingTransaction::new() + .auxiliary_data(aux_bytes) + .network_id(0) + .fee(180_000) + .build_conway_raw() + .expect("build_conway_raw"); + + // The body's auxiliary_data_hash should match the aux's + // compute_hash output. + let body = + pallas_primitives::conway::Tx::decode_fragment(tx.tx_bytes.as_ref()) + .expect("decode tx_bytes"); + let body_aux_hash_bytes: Vec = body + .transaction_body + .auxiliary_data_hash + .clone() + .expect("auxiliary_data_hash should be set when aux is attached") + .to_vec(); + assert_eq!(body_aux_hash_bytes, expected_hash.to_vec()); + + // And the auxiliary_data should round-trip back. + let pallas_aux = match body.auxiliary_data { + pallas_codec::utils::Nullable::Some(a) => a, + other => panic!("expected Some(aux), got {other:?}"), + }; + // Re-encode the aux that came back and compare. + let round_tripped_bytes = minicbor::to_vec(&pallas_aux).expect("re-encode aux"); + let original_again = minicbor::to_vec(&aux).expect("re-encode original"); + assert_eq!(round_tripped_bytes, original_again); + } + + #[test] + fn no_auxiliary_data_means_no_hash() { + let tx = StagingTransaction::new() + .network_id(0) + .fee(180_000) + .build_conway_raw() + .expect("build_conway_raw"); + let body = + pallas_primitives::conway::Tx::decode_fragment(tx.tx_bytes.as_ref()) + .expect("decode tx_bytes"); + assert!(body.transaction_body.auxiliary_data_hash.is_none()); + } +} diff --git a/pallas-txbuilder/src/transaction/model.rs b/pallas-txbuilder/src/transaction/model.rs index 5e1227e..b56e947 100644 --- a/pallas-txbuilder/src/transaction/model.rs +++ b/pallas-txbuilder/src/transaction/model.rs @@ -40,10 +40,14 @@ pub struct StagingTransaction { pub signature_amount_override: Option, pub change_address: Option
, pub language_view: Option, + /// CBOR-encoded `pallas_primitives::alonzo::AuxiliaryData`. Stored + /// as bytes rather than the typed enum because `AuxiliaryData` + /// doesn't implement `Eq`, which the rest of this struct requires. + /// `conway::build_conway_raw` decodes on the way out. + pub auxiliary_data: Option>, // pub certificates: TODO // pub withdrawals: TODO // pub updates: TODO - // pub auxiliary_data: TODO // pub phase_2_valid: TODO } @@ -372,6 +376,26 @@ impl StagingTransaction { self.change_address = None; self } + + /// Attach pre-encoded auxiliary data (CBOR bytes of an + /// `alonzo::AuxiliaryData`). The conway builder will decode on + /// build and compute `auxiliary_data_hash` automatically. + /// + /// Most callers use this for transaction metadata (CIP-25 / CIP-20 + /// / CIP-68) and/or to ship native scripts as auxiliary data. + /// Construct an `AuxiliaryData::PostAlonzo(...)` with the + /// metadata + scripts you want, encode with `Fragment::encode_fragment` + /// or `minicbor::to_vec`, and pass the bytes here. + pub fn auxiliary_data(mut self, cbor_bytes: Vec) -> Self { + self.auxiliary_data = Some(cbor_bytes); + self + } + + /// Drop any auxiliary data previously attached. + pub fn clear_auxiliary_data(mut self) -> Self { + self.auxiliary_data = None; + self + } } // TODO: Don't want our wrapper types in fields public diff --git a/pallas-txbuilder/src/transaction/serialise.rs b/pallas-txbuilder/src/transaction/serialise.rs index e60008f..28e0c08 100644 --- a/pallas-txbuilder/src/transaction/serialise.rs +++ b/pallas-txbuilder/src/transaction/serialise.rs @@ -480,6 +480,7 @@ mod tests { change_address: Some(Address(PallasAddress::from_str("addr1g9ekml92qyvzrjmawxkh64r2w5xr6mg9ngfmxh2khsmdrcudevsft64mf887333adamant").unwrap())), script_data_hash: Some(Bytes32([0; 32])), language_view: Some(crate::scriptdata::LanguageView(1, vec![1, 2, 3])), + auxiliary_data: None, }; let serialised_tx = serde_json::to_string(&tx).unwrap();