From aa54d5061b492beaaecf38c71e36ba8469ce493f Mon Sep 17 00:00:00 2001 From: Andrew Westberg Date: Thu, 13 Nov 2025 22:31:31 +0000 Subject: [PATCH] feat: Support hashed datum options for inputs and outputs --- .gitignore | 2 +- Cargo.lock | 41 ++++++++------------- crates/tx3-cardano/Cargo.toml | 4 +-- crates/tx3-cardano/src/compile/mod.rs | 51 ++++++++++++++++++--------- crates/tx3-cardano/src/tests.rs | 36 +++++++++++++++++++ crates/tx3-lang/src/analyzing.rs | 2 ++ crates/tx3-lang/src/applying.rs | 1 + crates/tx3-lang/src/ast.rs | 2 ++ crates/tx3-lang/src/ir.rs | 2 ++ crates/tx3-lang/src/lowering.rs | 36 ++++++++++++++++++- crates/tx3-lang/src/parsing.rs | 6 ++++ crates/tx3-lang/src/tx3.pest | 4 ++- examples/input_hashed_datum.tx3 | 29 +++++++++++++++ 13 files changed, 168 insertions(+), 48 deletions(-) create mode 100644 examples/input_hashed_datum.tx3 diff --git a/.gitignore b/.gitignore index 094a2708..79557244 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,6 @@ target/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ .scratchpad/ diff --git a/Cargo.lock b/Cargo.lock index 2865169c..8493993f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -986,8 +986,7 @@ checksum = "1036865bb9422d3300cf723f657c2851d0e9ab12567854b1f4eba3d77decf564" [[package]] name = "pallas" version = "1.0.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94c49f12ad9af4277b6d11c4ddc8044975ff1e634b1f5cd696f812eba9ac8f60" +source = "git+https://github.com/txpipe/pallas.git?rev=065df6542e376761d7b4bc90449493c8880f24c6#065df6542e376761d7b4bc90449493c8880f24c6" dependencies = [ "pallas-addresses", "pallas-codec", @@ -1004,8 +1003,7 @@ dependencies = [ [[package]] name = "pallas-addresses" version = "1.0.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f0bf697964d0562ee36727834f3fb698bdcf71dc2fef8c2f15e4e8920c6b55" +source = "git+https://github.com/txpipe/pallas.git?rev=065df6542e376761d7b4bc90449493c8880f24c6#065df6542e376761d7b4bc90449493c8880f24c6" dependencies = [ "base58", "bech32", @@ -1020,8 +1018,7 @@ dependencies = [ [[package]] name = "pallas-codec" version = "1.0.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51205265dc1f976a09e845abb4303e6704de54e377d3350ee30be70b26249f7f" +source = "git+https://github.com/txpipe/pallas.git?rev=065df6542e376761d7b4bc90449493c8880f24c6#065df6542e376761d7b4bc90449493c8880f24c6" dependencies = [ "hex", "minicbor", @@ -1032,8 +1029,7 @@ dependencies = [ [[package]] name = "pallas-configs" version = "1.0.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b280e2fe5e19e6dcee72d01313e5e754b753760980e5419647fe554e9540c739" +source = "git+https://github.com/txpipe/pallas.git?rev=065df6542e376761d7b4bc90449493c8880f24c6#065df6542e376761d7b4bc90449493c8880f24c6" dependencies = [ "base64 0.22.1", "num-rational", @@ -1048,13 +1044,12 @@ dependencies = [ [[package]] name = "pallas-crypto" version = "1.0.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d62094c2f70164cd878b4f3ae2f81f79aca79d7b3a2a5db2fa15d09002d5f7d" +source = "git+https://github.com/txpipe/pallas.git?rev=065df6542e376761d7b4bc90449493c8880f24c6#065df6542e376761d7b4bc90449493c8880f24c6" dependencies = [ "cryptoxide", "hex", "pallas-codec", - "rand_core 0.6.4", + "rand_core 0.9.3", "serde", "thiserror 1.0.69", ] @@ -1062,8 +1057,7 @@ dependencies = [ [[package]] name = "pallas-network" version = "1.0.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "689a470fe83eb0bc02014681a55614d183c8e339cae3126fcbe8a40cdad23aaf" +source = "git+https://github.com/txpipe/pallas.git?rev=065df6542e376761d7b4bc90449493c8880f24c6#065df6542e376761d7b4bc90449493c8880f24c6" dependencies = [ "byteorder", "hex", @@ -1080,8 +1074,7 @@ dependencies = [ [[package]] name = "pallas-primitives" version = "1.0.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db6f5bff8e1163e33dd116814c7288866928219cb84c90803f41b2d3ec0edc1" +source = "git+https://github.com/txpipe/pallas.git?rev=065df6542e376761d7b4bc90449493c8880f24c6#065df6542e376761d7b4bc90449493c8880f24c6" dependencies = [ "hex", "pallas-codec", @@ -1093,8 +1086,7 @@ dependencies = [ [[package]] name = "pallas-traverse" version = "1.0.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9121e2b43a76b06331e5527c2948da2eef5731f84499e5a3d737a62c7d96f6a" +source = "git+https://github.com/txpipe/pallas.git?rev=065df6542e376761d7b4bc90449493c8880f24c6#065df6542e376761d7b4bc90449493c8880f24c6" dependencies = [ "hex", "itertools 0.13.0", @@ -1110,8 +1102,7 @@ dependencies = [ [[package]] name = "pallas-txbuilder" version = "1.0.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0653ba4617846e812760f293aa339a27fcb957b595f2083da7887b1c094d33c" +source = "git+https://github.com/txpipe/pallas.git?rev=065df6542e376761d7b4bc90449493c8880f24c6#065df6542e376761d7b4bc90449493c8880f24c6" dependencies = [ "hex", "pallas-addresses", @@ -1127,8 +1118,7 @@ dependencies = [ [[package]] name = "pallas-utxorpc" version = "1.0.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feeb18eb3e249116a933dc33bfff11b93f58776e2ce81d06239eda612d22f23e" +source = "git+https://github.com/txpipe/pallas.git?rev=065df6542e376761d7b4bc90449493c8880f24c6#065df6542e376761d7b4bc90449493c8880f24c6" dependencies = [ "pallas-codec", "pallas-crypto", @@ -1136,14 +1126,13 @@ dependencies = [ "pallas-traverse", "pallas-validate", "prost-types", - "utxorpc-spec 0.16.0", + "utxorpc-spec 0.17.0", ] [[package]] name = "pallas-validate" version = "1.0.0-alpha.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840a71496d6b9255972cbab9a3ba9c16b07b74773f39767bec90420b99c0a389" +source = "git+https://github.com/txpipe/pallas.git?rev=065df6542e376761d7b4bc90449493c8880f24c6#065df6542e376761d7b4bc90449493c8880f24c6" dependencies = [ "chrono", "hex", @@ -2251,9 +2240,9 @@ dependencies = [ [[package]] name = "utxorpc-spec" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7148c5cd211c397a41245cfbd8d4cb8371d748ed5e3cf131ebcd9cacfa794206" +checksum = "a1d984ee351b308377e118135e638a5d544fdb0855f12a3b088d9dcaf0432052" dependencies = [ "bytes", "futures-core", diff --git a/crates/tx3-cardano/Cargo.toml b/crates/tx3-cardano/Cargo.toml index a1a2b156..0ddecc66 100644 --- a/crates/tx3-cardano/Cargo.toml +++ b/crates/tx3-cardano/Cargo.toml @@ -13,9 +13,9 @@ homepage.workspace = true readme.workspace = true [dependencies] -pallas = { version = ">=1.0.0-alpha, <2.0.0" } +# pallas = { version = ">=1.0.0-alpha, <2.0.0" } # pallas = { version = ">=1.0.0-alpha, <2.0.0", path = "../../../../txpipe/pallas/pallas" } -# pallas = { git = "https://github.com/txpipe/pallas.git" } +pallas = { git = "https://github.com/txpipe/pallas.git", rev = "065df6542e376761d7b4bc90449493c8880f24c6" } hex = "0.4.3" thiserror = "2.0.11" diff --git a/crates/tx3-cardano/src/compile/mod.rs b/crates/tx3-cardano/src/compile/mod.rs index fcf441c2..d91cbe9f 100644 --- a/crates/tx3-cardano/src/compile/mod.rs +++ b/crates/tx3-cardano/src/compile/mod.rs @@ -203,13 +203,19 @@ fn compile_output_block( let datum_option = ir.datum.as_option().map(compile_data_expr).transpose()?; + let datum_option = datum_option.map(|datum| { + if ir.datum_hash_mode { + primitives::DatumOption::Hash(datum.compute_hash()).into() + } else { + primitives::DatumOption::Data(pallas::codec::utils::CborWrap(datum.into())).into() + } + }); + let output = primitives::TransactionOutput::PostAlonzo( primitives::PostAlonzoTransactionOutput { address: address.to_vec().into(), value, - datum_option: datum_option.map(|x| { - primitives::DatumOption::Data(pallas::codec::utils::CborWrap(x.into())).into() - }), + datum_option, script_ref: None, } .into(), @@ -868,6 +874,19 @@ fn compile_adhoc_native_witness(tx: &ir::Tx) -> Result, Error .collect::, _>>() } +pub type DatumWitness = KeepRaw<'static, primitives::PlutusData>; + +fn compile_datum_witnesses(tx: &ir::Tx) -> Result, Error> { + // TODO: also lookup and include datum witnesses from inputs that have datum hashes + + tx.outputs + .iter() + .filter(|x| x.datum_hash_mode) + .filter_map(|output| output.datum.as_option().map(compile_data_expr)) + .map(|result| result.map(KeepRaw::from)) + .collect::, _>>() +} + fn compile_witness_set( tx: &ir::Tx, compiled_body: &primitives::TransactionBody, @@ -888,7 +907,7 @@ fn compile_witness_set( vkeywitness: None, native_script: NonEmptySet::from_vec(compile_adhoc_native_witness(tx)?), bootstrap_witness: None, - plutus_data: None, + plutus_data: NonEmptySet::from_vec(compile_datum_witnesses(tx)?).map(KeepRaw::from), plutus_v1_script: NonEmptySet::from_vec(compile_adhoc_plutus_witness::<1>(tx)), plutus_v2_script: NonEmptySet::from_vec(compile_adhoc_plutus_witness::<2>(tx)), plutus_v3_script: NonEmptySet::from_vec(compile_adhoc_plutus_witness::<3>(tx)), @@ -897,19 +916,17 @@ fn compile_witness_set( Ok(witness_set) } -fn infer_plutus_version(witness_set: &primitives::WitnessSet) -> PlutusVersion { +fn infer_plutus_version(witness_set: &primitives::WitnessSet) -> Option { // TODO: how do we handle this for reference scripts? if witness_set.plutus_v1_script.is_some() { - 0 + Some(0) } else if witness_set.plutus_v2_script.is_some() { - 1 + Some(1) } else if witness_set.plutus_v3_script.is_some() { - 2 + Some(2) } else { - // TODO: should we error here? - // Defaulting to Plutus V3 for now - 2 + None } } @@ -919,11 +936,12 @@ fn compute_script_data_hash( ) -> Option> { let version = infer_plutus_version(witness_set); - let cost_model = pparams.cost_models.get(&version).unwrap(); - - let language_view = primitives::LanguageView(version, cost_model.clone()); + let language_view = version.map(|v|{ + let cost_model = pparams.cost_models.get(&v).unwrap(); + primitives::LanguageView(v, cost_model.clone()) + }); - let data = primitives::ScriptData::build_for(witness_set, language_view); + let data = primitives::ScriptData::build_for(witness_set, &language_view); data.map(|x| x.hash()) } @@ -957,8 +975,7 @@ pub fn entry_point(tx: &ir::Tx, pparams: &PParams) -> Result x.analyze(parent), OutputBlockField::Amount(x) => x.analyze(parent), OutputBlockField::Datum(x) => x.analyze(parent), + OutputBlockField::HashedDatum(x) => x.analyze(parent), } } @@ -1017,6 +1018,7 @@ impl Analyzable for OutputBlockField { OutputBlockField::To(x) => x.is_resolved(), OutputBlockField::Amount(x) => x.is_resolved(), OutputBlockField::Datum(x) => x.is_resolved(), + OutputBlockField::HashedDatum(x) => x.is_resolved(), } } } diff --git a/crates/tx3-lang/src/applying.rs b/crates/tx3-lang/src/applying.rs index f517be69..759cac94 100644 --- a/crates/tx3-lang/src/applying.rs +++ b/crates/tx3-lang/src/applying.rs @@ -1153,6 +1153,7 @@ impl Composite for ir::Output { Ok(Self { address: f(self.address)?, datum: f(self.datum)?, + datum_hash_mode: self.datum_hash_mode, amount: f(self.amount)?, optional: self.optional, }) diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index b8620754..5f9402a4 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -387,6 +387,7 @@ pub enum OutputBlockField { To(Box), Amount(Box), Datum(Box), + HashedDatum(Box), } impl OutputBlockField { @@ -395,6 +396,7 @@ impl OutputBlockField { OutputBlockField::To(_) => "to", OutputBlockField::Amount(_) => "amount", OutputBlockField::Datum(_) => "datum", + OutputBlockField::HashedDatum(_) => "hashed_datum", } } } diff --git a/crates/tx3-lang/src/ir.rs b/crates/tx3-lang/src/ir.rs index 9c769c3e..510ccd25 100644 --- a/crates/tx3-lang/src/ir.rs +++ b/crates/tx3-lang/src/ir.rs @@ -315,6 +315,7 @@ pub struct Input { pub struct Output { pub address: Expression, pub datum: Expression, + pub datum_hash_mode: bool, pub amount: Expression, pub optional: bool, } @@ -536,6 +537,7 @@ impl Node for Output { let visited = Self { address: self.address.apply(visitor)?, datum: self.datum.apply(visitor)?, + datum_hash_mode: self.datum_hash_mode, amount: self.amount.apply(visitor)?, optional: self.optional, }; diff --git a/crates/tx3-lang/src/lowering.rs b/crates/tx3-lang/src/lowering.rs index 32abf290..5fe3bed9 100644 --- a/crates/tx3-lang/src/lowering.rs +++ b/crates/tx3-lang/src/lowering.rs @@ -92,6 +92,7 @@ fn coerce_identifier_into_asset_def(identifier: &ast::Identifier) -> Result Self { + Self { + is_asset_expr: false, + is_datum_expr: false, + is_hashed_datum_expr: true, is_address_expr: false, } } @@ -116,6 +128,7 @@ impl Context { Self { is_asset_expr: false, is_datum_expr: false, + is_hashed_datum_expr: false, is_address_expr: true, } } @@ -131,6 +144,10 @@ impl Context { pub fn is_datum_expr(&self) -> bool { self.is_datum_expr } + + pub fn is_hashed_datum_expr(&self) -> bool { + self.is_hashed_datum_expr + } } pub(crate) trait IntoLower { @@ -619,6 +636,10 @@ impl IntoLower for ast::OutputBlockField { let ctx = ctx.enter_datum_expr(); x.into_lower(&ctx) } + ast::OutputBlockField::HashedDatum(x) => { + let ctx = ctx.enter_hashed_datum_expr(); + x.into_lower(&ctx) + } } } } @@ -628,12 +649,25 @@ impl IntoLower for ast::OutputBlock { fn into_lower(&self, ctx: &Context) -> Result { let address = self.find("to").into_lower(ctx)?.unwrap_or_default(); - let datum = self.find("datum").into_lower(ctx)?.unwrap_or_default(); + let datum = self.find("datum").into_lower(ctx)?; + let hashed_datum = self.find("hashed_datum").into_lower(ctx)?; let amount = self.find("amount").into_lower(ctx)?.unwrap_or_default(); + let (datum, datum_hash_mode) = match (datum, hashed_datum) { + (Some(_), Some(_)) => { + return Err(Error::InvalidAst( + "Both datum and hashed_datum cannot be set simultaneously".to_string(), + )) + }, + (Some(datum), None) => (datum, false), + (None, Some(hashed_datum)) => (hashed_datum, true), + (None, None) => (ir::Expression::default(), false), + }; + Ok(ir::Output { address, datum, + datum_hash_mode, amount, optional: self.optional, }) diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 1e67ca80..eabc0394 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -569,6 +569,11 @@ impl AstNode for OutputBlockField { let x = OutputBlockField::Datum(DataExpr::parse(pair)?.into()); Ok(x) } + Rule::output_block_hashed_datum => { + let pair = pair.into_inner().next().unwrap(); + let x = OutputBlockField::HashedDatum(DataExpr::parse(pair)?.into()); + Ok(x) + } x => unreachable!("Unexpected rule in output_block_field: {:?}", x), } } @@ -578,6 +583,7 @@ impl AstNode for OutputBlockField { Self::To(x) => x.span(), Self::Amount(x) => x.span(), Self::Datum(x) => x.span(), + Self::HashedDatum(x) => x.span(), } } } diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index 43ef45c1..b23111eb 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -259,11 +259,13 @@ reference_block = { output_block_to = { "to" ~ ":" ~ data_expr } output_block_amount = { "amount" ~ ":" ~ data_expr } output_block_datum = { "datum" ~ ":" ~ data_expr } +output_block_hashed_datum = { "hashed_datum" ~ ":" ~ data_expr } output_block_field = _{ output_block_to | output_block_amount | - output_block_datum + output_block_datum | + output_block_hashed_datum } output_block = { diff --git a/examples/input_hashed_datum.tx3 b/examples/input_hashed_datum.tx3 new file mode 100644 index 00000000..fa4b1114 --- /dev/null +++ b/examples/input_hashed_datum.tx3 @@ -0,0 +1,29 @@ +type MyRecord { + counter: Int, + other_field: Bytes, +} + +party MyParty; + +tx increase_counter() { + input source { + from: MyParty, + min_amount: fees, + datum_is: MyRecord, + } + + output { + to: MyParty, + amount: source - fees, + +// FIXME: We can't seem to reference source fields here yet. +// hashed_datum: MyRecord { +// counter: source.counter + 1, +// other_field: source.other_field, +// + hashed_datum: MyRecord { + counter: 2, + other_field: "abc", + }, + } +} \ No newline at end of file