From 166fb2af666e9d4777971b834b7353222a0620a2 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Thu, 4 Sep 2025 14:41:04 -0300 Subject: [PATCH 1/4] feat(ir): add ComputeTipSlot operation --- crates/tx3-cardano/src/lib.rs | 41 +++++++++++++++++++++++++++++++++ crates/tx3-lang/src/applying.rs | 11 +++++---- crates/tx3-lang/src/ir.rs | 2 ++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/crates/tx3-cardano/src/lib.rs b/crates/tx3-cardano/src/lib.rs index d8128b9b..94331cac 100644 --- a/crates/tx3-cardano/src/lib.rs +++ b/crates/tx3-cardano/src/lib.rs @@ -34,6 +34,24 @@ pub const EXECUTION_UNITS: primitives::ExUnits = primitives::ExUnits { const DEFAULT_EXTRA_FEES: u64 = 200_000; const MIN_UTXO_BYTES: i128 = 197; +struct SlotConfig { + zero_time: i64, + zero_slot: i64, + slot_length: i64, +} + +const MAINNET_SLOT_CONFIG: SlotConfig = SlotConfig { + zero_time: 1596059091000, + zero_slot: 4492800, + slot_length: 1000, +}; + +const TESTNET_SLOT_CONFIG: SlotConfig = SlotConfig { + zero_time: 1666656000000, + zero_slot: 0, + slot_length: 1000, +}; + #[derive(Debug, Clone, Default)] pub struct Config { pub extra_fees: Option, @@ -100,10 +118,33 @@ impl tx3_lang::backend::Compiler for Compiler { amount: ir::Expression::Number(lovelace), }])) } + ir::CompilerOp::ComputeTipSlot => { + let slot = compute_tip_slot(self.pparams.network)?; + Ok(ir::Expression::Number(slot as i128)) + } } } } +fn compute_tip_slot(network: Network) -> Result { + use std::time::{SystemTime, UNIX_EPOCH}; + + let current_time_ms = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| tx3_lang::backend::Error::CantReduce(ir::CompilerOp::ComputeTipSlot))? + .as_millis() as i64; + + let config = match network { + Network::Mainnet => &MAINNET_SLOT_CONFIG, + Network::Testnet => &TESTNET_SLOT_CONFIG, + }; + + let elapsed_time = current_time_ms - config.zero_time; + let slot = config.zero_slot + (elapsed_time / config.slot_length); + + Ok(slot.max(0)) +} + fn eval_size_fees(tx: &[u8], pparams: &PParams, extra_fees: Option) -> u64 { tx.len() as u64 * pparams.min_fee_coefficient + pparams.min_fee_constant diff --git a/crates/tx3-lang/src/applying.rs b/crates/tx3-lang/src/applying.rs index 391b487f..7d67d8ac 100644 --- a/crates/tx3-lang/src/applying.rs +++ b/crates/tx3-lang/src/applying.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, HashMap, HashSet}; -use crate::{backend, ir, ArgValue, CanonicalAssets, Utxo}; use crate::ir::Expression; +use crate::{backend, ir, ArgValue, CanonicalAssets, Utxo}; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -186,7 +186,7 @@ impl Concatenable for Vec { match other { Expression::List(expressions) => { Ok(Expression::List([&self[..], &expressions[..]].concat())) - }, + } _ => Err(Error::InvalidBinaryOp( "concat".to_string(), format!("List({:?})", self), @@ -1143,6 +1143,7 @@ impl Composite for ir::CompilerOp { match self { ir::CompilerOp::BuildScriptAddress(x) => vec![x], ir::CompilerOp::ComputeMinUtxo(x) => vec![x], + ir::CompilerOp::ComputeTipSlot => vec![], } } @@ -1153,6 +1154,7 @@ impl Composite for ir::CompilerOp { match self { ir::CompilerOp::BuildScriptAddress(x) => Ok(ir::CompilerOp::BuildScriptAddress(f(x)?)), ir::CompilerOp::ComputeMinUtxo(x) => Ok(ir::CompilerOp::ComputeMinUtxo(f(x)?)), + ir::CompilerOp::ComputeTipSlot => Ok(ir::CompilerOp::ComputeTipSlot), } } } @@ -1874,8 +1876,9 @@ mod tests { let reduced = op.reduce().unwrap(); match reduced { - ir::Expression::List(b) => assert_eq!(b, vec![ - Expression::Number(1), Expression::Number(2)]), + ir::Expression::List(b) => { + assert_eq!(b, vec![Expression::Number(1), Expression::Number(2)]) + } _ => panic!("Expected List [Number(1), Number(2)"), } } diff --git a/crates/tx3-lang/src/ir.rs b/crates/tx3-lang/src/ir.rs index ba1991e8..d44a97db 100644 --- a/crates/tx3-lang/src/ir.rs +++ b/crates/tx3-lang/src/ir.rs @@ -72,6 +72,7 @@ pub enum BuiltInOp { pub enum CompilerOp { BuildScriptAddress(Expression), ComputeMinUtxo(Expression), + ComputeTipSlot, } #[derive(Encode, Decode, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] @@ -467,6 +468,7 @@ impl Node for CompilerOp { let visited = match self { CompilerOp::BuildScriptAddress(x) => CompilerOp::BuildScriptAddress(x.apply(visitor)?), CompilerOp::ComputeMinUtxo(x) => CompilerOp::ComputeMinUtxo(x.apply(visitor)?), + CompilerOp::ComputeTipSlot => CompilerOp::ComputeTipSlot, }; Ok(visited) From 4fb12f7543393bd4a9bc39ac36a63673988db759 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Fri, 5 Sep 2025 15:59:03 -0300 Subject: [PATCH 2/4] feat(lang): add ComputeTipSlot operation and related parsing support --- crates/tx3-cardano/src/lib.rs | 46 ++++++--------------------------- crates/tx3-cardano/src/tests.rs | 7 ++++- crates/tx3-lang/src/ast.rs | 2 ++ crates/tx3-lang/src/lowering.rs | 3 +++ crates/tx3-lang/src/parsing.rs | 24 +++++++++++++++++ crates/tx3-lang/src/tx3.pest | 7 +++-- examples/tip_slot.tx3 | 22 ++++++++++++++++ 7 files changed, 70 insertions(+), 41 deletions(-) create mode 100644 examples/tip_slot.tx3 diff --git a/crates/tx3-cardano/src/lib.rs b/crates/tx3-cardano/src/lib.rs index 94331cac..0e63a873 100644 --- a/crates/tx3-cardano/src/lib.rs +++ b/crates/tx3-cardano/src/lib.rs @@ -34,27 +34,10 @@ pub const EXECUTION_UNITS: primitives::ExUnits = primitives::ExUnits { const DEFAULT_EXTRA_FEES: u64 = 200_000; const MIN_UTXO_BYTES: i128 = 197; -struct SlotConfig { - zero_time: i64, - zero_slot: i64, - slot_length: i64, -} - -const MAINNET_SLOT_CONFIG: SlotConfig = SlotConfig { - zero_time: 1596059091000, - zero_slot: 4492800, - slot_length: 1000, -}; - -const TESTNET_SLOT_CONFIG: SlotConfig = SlotConfig { - zero_time: 1666656000000, - zero_slot: 0, - slot_length: 1000, -}; - #[derive(Debug, Clone, Default)] pub struct Config { pub extra_fees: Option, + pub tip_slot: Option, } pub type TxBody = @@ -119,32 +102,19 @@ impl tx3_lang::backend::Compiler for Compiler { }])) } ir::CompilerOp::ComputeTipSlot => { - let slot = compute_tip_slot(self.pparams.network)?; + let slot = self + .config + .tip_slot + .map(|slot| slot as i64) + .ok_or_else(|| { + tx3_lang::backend::Error::CantReduce(ir::CompilerOp::ComputeTipSlot) + })?; Ok(ir::Expression::Number(slot as i128)) } } } } -fn compute_tip_slot(network: Network) -> Result { - use std::time::{SystemTime, UNIX_EPOCH}; - - let current_time_ms = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|_| tx3_lang::backend::Error::CantReduce(ir::CompilerOp::ComputeTipSlot))? - .as_millis() as i64; - - let config = match network { - Network::Mainnet => &MAINNET_SLOT_CONFIG, - Network::Testnet => &TESTNET_SLOT_CONFIG, - }; - - let elapsed_time = current_time_ms - config.zero_time; - let slot = config.zero_slot + (elapsed_time / config.slot_length); - - Ok(slot.max(0)) -} - fn eval_size_fees(tx: &[u8], pparams: &PParams, extra_fees: Option) -> u64 { tx.len() as u64 * pparams.min_fee_coefficient + pparams.min_fee_constant diff --git a/crates/tx3-cardano/src/tests.rs b/crates/tx3-cardano/src/tests.rs index fc07e628..1b410879 100644 --- a/crates/tx3-cardano/src/tests.rs +++ b/crates/tx3-cardano/src/tests.rs @@ -46,7 +46,10 @@ fn test_compiler(config: Option) -> Compiler { ]), }; - let config = config.unwrap_or(Config { extra_fees: None }); + let config = config.unwrap_or(Config { + extra_fees: None, + tip_slot: None, + }); Compiler::new(pparams, config) } @@ -431,6 +434,7 @@ async fn extra_fees_test() { let mut compiler = test_compiler(Some(Config { extra_fees: Some(extra_fees), + tip_slot: None, })); let utxos = wildcard_utxos(None); @@ -454,6 +458,7 @@ async fn extra_fees_test() { async fn extra_fees_zero_test() { let mut compiler = test_compiler(Some(Config { extra_fees: Some(0), + tip_slot: None, })); let utxos = wildcard_utxos(None); diff --git a/crates/tx3-lang/src/ast.rs b/crates/tx3-lang/src/ast.rs index f19c5537..ab5b0446 100644 --- a/crates/tx3-lang/src/ast.rs +++ b/crates/tx3-lang/src/ast.rs @@ -699,6 +699,7 @@ pub enum DataExpr { AnyAssetConstructor(AnyAssetConstructor), Identifier(Identifier), MinUtxo(Identifier), + ComputeTipSlot, AddOp(AddOp), SubOp(SubOp), ConcatOp(ConcatOp), @@ -735,6 +736,7 @@ impl DataExpr { DataExpr::AnyAssetConstructor(x) => x.target_type(), DataExpr::UtxoRef(_) => Some(Type::UtxoRef), DataExpr::MinUtxo(_) => Some(Type::AnyAsset), + DataExpr::ComputeTipSlot => Some(Type::Int), } } } diff --git a/crates/tx3-lang/src/lowering.rs b/crates/tx3-lang/src/lowering.rs index 1ba43c8c..bfc0c37b 100644 --- a/crates/tx3-lang/src/lowering.rs +++ b/crates/tx3-lang/src/lowering.rs @@ -449,6 +449,9 @@ impl IntoLower for ast::DataExpr { ast::DataExpr::MinUtxo(x) => ir::Expression::EvalCompiler(Box::new( ir::CompilerOp::ComputeMinUtxo(x.into_lower(ctx)?), )), + ast::DataExpr::ComputeTipSlot => { + ir::Expression::EvalCompiler(Box::new(ir::CompilerOp::ComputeTipSlot)) + } }; Ok(out) diff --git a/crates/tx3-lang/src/parsing.rs b/crates/tx3-lang/src/parsing.rs index 0a2db0bf..065854b4 100644 --- a/crates/tx3-lang/src/parsing.rs +++ b/crates/tx3-lang/src/parsing.rs @@ -1094,6 +1094,10 @@ impl DataExpr { Ok(DataExpr::MinUtxo(Identifier::parse(inner)?)) } + fn tip_slot_parse(_pair: Pair) -> Result { + Ok(DataExpr::ComputeTipSlot) + } + fn concat_constructor_parse(pair: Pair) -> Result { Ok(DataExpr::ConcatOp(ConcatOp::parse(pair)?)) } @@ -1166,6 +1170,7 @@ impl AstNode for DataExpr { Rule::any_asset_constructor => DataExpr::any_asset_constructor_parse(x), Rule::concat_constructor => DataExpr::concat_constructor_parse(x), Rule::min_utxo => DataExpr::min_utxo_parse(x), + Rule::tip_slot => DataExpr::tip_slot_parse(x), Rule::data_expr => DataExpr::parse(x), x => unreachable!("unexpected rule as data primary: {:?}", x), }) @@ -1205,6 +1210,7 @@ impl AstNode for DataExpr { DataExpr::PropertyOp(x) => &x.span, DataExpr::UtxoRef(x) => x.span(), DataExpr::MinUtxo(x) => x.span(), + DataExpr::ComputeTipSlot => &Span::DUMMY, // TODO } } } @@ -1971,6 +1977,24 @@ mod tests { }) ); + input_to_ast_check!( + DataExpr, + "tip_slot_basic", + "tip_slot()", + DataExpr::ComputeTipSlot + ); + + input_to_ast_check!( + DataExpr, + "tip_slot_in_expression", + "1000 + tip_slot()", + DataExpr::AddOp(AddOp { + lhs: Box::new(DataExpr::Number(1000)), + rhs: Box::new(DataExpr::ComputeTipSlot), + span: Span::DUMMY, + }) + ); + input_to_ast_check!( StructConstructor, "struct_constructor_record", diff --git a/crates/tx3-lang/src/tx3.pest b/crates/tx3-lang/src/tx3.pest index 7d1acd99..0dbe6192 100644 --- a/crates/tx3-lang/src/tx3.pest +++ b/crates/tx3-lang/src/tx3.pest @@ -126,6 +126,8 @@ concat_constructor = { min_utxo = { "min_utxo" ~ "(" ~ identifier ~ ")" } +tip_slot = { "tip_slot" ~ "(" ~ ")" } + data_expr = { data_prefix* ~ data_primary ~ data_postfix* ~ (data_infix ~ data_prefix* ~ data_primary ~ data_postfix* )* } data_infix = _{ data_add | data_sub } @@ -134,7 +136,7 @@ data_expr = { data_prefix* ~ data_primary ~ data_postfix* ~ (data_infix ~ data_p data_prefix = _{ data_negate } data_negate = { "!" } - + data_postfix = _{ data_property } data_property = { "." ~ identifier } @@ -146,6 +148,7 @@ data_expr = { data_prefix* ~ data_primary ~ data_postfix* ~ (data_infix ~ data_p bool | string | min_utxo | + tip_slot | struct_constructor | list_constructor | concat_constructor | @@ -360,7 +363,7 @@ cardano_block = { cardano_vote_delegation_certificate | cardano_withdrawal_block | cardano_plutus_witness_block | - cardano_native_witness_block | + cardano_native_witness_block | cardano_treasury_donation_block ) } diff --git a/examples/tip_slot.tx3 b/examples/tip_slot.tx3 new file mode 100644 index 00000000..fd186a02 --- /dev/null +++ b/examples/tip_slot.tx3 @@ -0,0 +1,22 @@ +party Sender; + +type TimestampDatum { + current_slot: Int, + expiry_slot: Int, +} + +tx create_timestamp_tx(validity_period: Int) { + input source { + from: Sender, + min_amount: Ada(2000000), + } + + output timestamp_output { + to: Sender, + amount: source - fees, + datum: TimestampDatum { + current_slot: tip_slot(), + expiry_slot: tip_slot() + validity_period, + }, + } +} From 98a8309a93deec17ef5da1ce1b8e1235a4d847f1 Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Tue, 9 Sep 2025 16:07:08 -0300 Subject: [PATCH 3/4] feat(compiler): add ChainTip struct and update Compiler --- crates/tx3-cardano/src/lib.rs | 22 ++++++++++------------ crates/tx3-cardano/src/tests.rs | 18 ++++++++++-------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/crates/tx3-cardano/src/lib.rs b/crates/tx3-cardano/src/lib.rs index 0e63a873..c6524227 100644 --- a/crates/tx3-cardano/src/lib.rs +++ b/crates/tx3-cardano/src/lib.rs @@ -37,24 +37,31 @@ const MIN_UTXO_BYTES: i128 = 197; #[derive(Debug, Clone, Default)] pub struct Config { pub extra_fees: Option, - pub tip_slot: Option, } pub type TxBody = pallas::codec::utils::KeepRaw<'static, primitives::conway::TransactionBody<'static>>; +#[derive(Debug, Clone)] +pub struct ChainTip { + pub slot: u64, + pub hash: Vec, +} + pub struct Compiler { pub pparams: PParams, pub config: Config, pub latest_tx_body: Option, + pub cursor: ChainTip, } impl Compiler { - pub fn new(pparams: PParams, config: Config) -> Self { + pub fn new(pparams: PParams, config: Config, cursor: ChainTip) -> Self { Self { pparams, config, latest_tx_body: None, + cursor, } } } @@ -101,16 +108,7 @@ impl tx3_lang::backend::Compiler for Compiler { amount: ir::Expression::Number(lovelace), }])) } - ir::CompilerOp::ComputeTipSlot => { - let slot = self - .config - .tip_slot - .map(|slot| slot as i64) - .ok_or_else(|| { - tx3_lang::backend::Error::CantReduce(ir::CompilerOp::ComputeTipSlot) - })?; - Ok(ir::Expression::Number(slot as i128)) - } + ir::CompilerOp::ComputeTipSlot => Ok(ir::Expression::Number(self.cursor.slot as i128)), } } } diff --git a/crates/tx3-cardano/src/tests.rs b/crates/tx3-cardano/src/tests.rs index 1b410879..4e642753 100644 --- a/crates/tx3-cardano/src/tests.rs +++ b/crates/tx3-cardano/src/tests.rs @@ -46,12 +46,16 @@ fn test_compiler(config: Option) -> Compiler { ]), }; - let config = config.unwrap_or(Config { - extra_fees: None, - tip_slot: None, - }); - - Compiler::new(pparams, config) + let config = config.unwrap_or(Config { extra_fees: None }); + + Compiler::new( + pparams, + config, + ChainTip { + slot: 101674141, + hash: vec![], + }, + ) } fn load_protocol(example_name: &str) -> Protocol { @@ -434,7 +438,6 @@ async fn extra_fees_test() { let mut compiler = test_compiler(Some(Config { extra_fees: Some(extra_fees), - tip_slot: None, })); let utxos = wildcard_utxos(None); @@ -458,7 +461,6 @@ async fn extra_fees_test() { async fn extra_fees_zero_test() { let mut compiler = test_compiler(Some(Config { extra_fees: Some(0), - tip_slot: None, })); let utxos = wildcard_utxos(None); From bfc8c438e7b1dbc60513583875774f60326fd59f Mon Sep 17 00:00:00 2001 From: sofia-bobbiesi Date: Thu, 11 Sep 2025 12:14:56 -0300 Subject: [PATCH 4/4] refactor: rename ChainTip to ChainPoint in Compiler and tests --- crates/tx3-cardano/src/lib.rs | 6 +++--- crates/tx3-cardano/src/tests.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/tx3-cardano/src/lib.rs b/crates/tx3-cardano/src/lib.rs index c6524227..322b1848 100644 --- a/crates/tx3-cardano/src/lib.rs +++ b/crates/tx3-cardano/src/lib.rs @@ -43,7 +43,7 @@ pub type TxBody = pallas::codec::utils::KeepRaw<'static, primitives::conway::TransactionBody<'static>>; #[derive(Debug, Clone)] -pub struct ChainTip { +pub struct ChainPoint { pub slot: u64, pub hash: Vec, } @@ -52,11 +52,11 @@ pub struct Compiler { pub pparams: PParams, pub config: Config, pub latest_tx_body: Option, - pub cursor: ChainTip, + pub cursor: ChainPoint, } impl Compiler { - pub fn new(pparams: PParams, config: Config, cursor: ChainTip) -> Self { + pub fn new(pparams: PParams, config: Config, cursor: ChainPoint) -> Self { Self { pparams, config, diff --git a/crates/tx3-cardano/src/tests.rs b/crates/tx3-cardano/src/tests.rs index 4e642753..8aaf2f44 100644 --- a/crates/tx3-cardano/src/tests.rs +++ b/crates/tx3-cardano/src/tests.rs @@ -51,7 +51,7 @@ fn test_compiler(config: Option) -> Compiler { Compiler::new( pparams, config, - ChainTip { + ChainPoint { slot: 101674141, hash: vec![], },