From 57c96dd0955b255dd04eeeb02c356959479acdb4 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Wed, 12 Aug 2026 17:22:30 +0400 Subject: [PATCH 1/8] feat(transaction)!: make max_epoch mandatory and bound the validity window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Motivation `max_epoch` was optional with no upper bound, so a signed transaction stayed sequenceable forever. Two consequences: a wallet could never declare a transaction dead, and an aborted attempt — which consensus deliberately allows to be re-sequenced — could be retried indefinitely. Committed-id replay is already handled permanently by the receipt-existence gate, so this is not about replay protection. It is about giving every transaction a bounded, deterministic lifetime. ## Changes - `max_epoch: Option` → `Epoch` on `UnsignedTransactionV1`, its pruned mirror, the signing-domain projection and the transaction pool record. The builder takes it at construction (`Transaction::builder(network, max_epoch)`), so every construction site chooses a window at compile time. - New consensus constant `max_transaction_validity_epochs = 2160` — ~30 days at the ~20 minute epoch target, uniform across networks. A ceiling, not a default. Covered by a test asserting network uniformity. - `EpochRangeValidator` gains the upper bound and is constructed with the constant. `MaxEpochTooFarAhead` is classified node-local, not sender fault: a node lagging by an epoch would otherwise penalise honest senders whose window sits at the ceiling. - Mempool ingress runs the epoch rules alongside the structural ones, before the gossip-acceptance verdict, so an out-of-window transaction is refused before it is admitted or re-gossiped. Consensus sequencing already routed through the same validator. - Wallet daemon stamps `max_epoch = current_epoch + default_transaction_validity_epochs` (new config, default 3 ≈ 1h) when the caller does not supply one, backed by a 30s epoch cache. Building now fails if the indexer is unreachable rather than guessing a window. `settings.get` exposes the current epoch and the default window so callers that build transactions themselves (wallet CLI, traffic-sim) can pick a window the network accepts. - Ledger: `SigningField::MaxEpoch` now carries a bare little-endian u64 rather than a borsh `Option`, and the device always displays it. `epoch_history_length` is deliberately left alone: committed-id dedup reads the receipt substate, which is chain state and retained regardless, so retention length has no bearing on the window. ## Deployment notes Consensus-breaking and a signing-domain break — the `max_epoch` field encoding changes, so pre-signed transactions and Ledger golden vectors do not carry over. Belongs in the same breaking batch as the other consensus changes. ## Verification - 6 new `EpochRangeValidator` unit tests (min, expiry, ceiling, inclusive boundary, saturating ceiling). - Ledger lock-step tests green (`preimage_field_tags_match_protocol` and the three recipe tests). - SDK golden vectors regenerated. - `cargo lints clippy --all-targets` clean; `cargo nextest r -E "not package(integration_tests)" --release` → 1772 passed, 0 failed. Co-Authored-By: Claude Opus 5 --- .../src/storage_sqlite/store_factory.rs | 2 +- .../tari_validator_node/src/bootstrap.rs | 3 +- .../src/p2p/services/mempool/initializer.rs | 7 +- .../src/p2p/services/mempool/service.rs | 21 ++- .../src/command/transaction.rs | 37 +++-- .../examples/transaction_request_flow.rs | 4 +- applications/tari_walletd/src/config.rs | 15 ++ .../tari_walletd/src/handlers/accounts.rs | 15 +- .../tari_walletd/src/handlers/context.rs | 49 ++++++- .../tari_walletd/src/handlers/nfts.rs | 3 +- .../tari_walletd/src/handlers/settings.rs | 4 + .../tari_walletd/src/handlers/transaction.rs | 11 +- .../tari_walletd/src/handlers/validator.rs | 2 +- .../src/services/auto_claim_burn_service.rs | 15 ++ .../src/types/PrunedUnsignedTransactionV1.ts | 2 +- bindings/src/types/TransactionPoolRecord.ts | 2 +- bindings/src/types/UnsignedTransactionV1.ts | 2 +- .../src/types/wallet-types/InputSelection.ts | 5 +- .../types/wallet-types/SettingsGetResponse.ts | 2 + clients/wallet_daemon_client/src/types.rs | 9 ++ crates/consensus/src/consensus_constants.rs | 31 ++++ .../hotstuff/transaction_manager/manager.rs | 10 +- crates/consensus_tests/src/consensus.rs | 38 ++--- crates/consensus_tests/src/support/harness.rs | 1 + .../src/support/transaction.rs | 2 +- .../examples/native_points_calibrate.rs | 4 +- crates/engine/tests/access_rules.rs | 136 +++++++++--------- crates/engine/tests/account.rs | 26 ++-- crates/engine/tests/address_allocation.rs | 16 +-- crates/engine/tests/airdrop.rs | 8 +- crates/engine/tests/asserts.rs | 28 ++-- crates/engine/tests/burn.rs | 6 +- crates/engine/tests/complex_fee_payment.rs | 8 +- crates/engine/tests/compute_fee_budget.rs | 14 +- crates/engine/tests/confidential.rs | 30 ++-- crates/engine/tests/cross_template.rs | 16 +-- crates/engine/tests/events.rs | 6 +- crates/engine/tests/fees.rs | 28 ++-- crates/engine/tests/freeze.rs | 12 +- crates/engine/tests/fungible.rs | 16 +-- crates/engine/tests/guessing_game.rs | 14 +- crates/engine/tests/limits.rs | 8 +- crates/engine/tests/metering_budget.rs | 4 +- crates/engine/tests/native_compute_budget.rs | 14 +- crates/engine/tests/no_concurrency.rs | 4 +- crates/engine/tests/no_std.rs | 8 +- crates/engine/tests/publish_template.rs | 10 +- crates/engine/tests/recall.rs | 8 +- crates/engine/tests/reentrancy.rs | 12 +- crates/engine/tests/shenanigans.rs | 46 +++--- crates/engine/tests/signature.rs | 12 +- crates/engine/tests/spend_script.rs | 70 ++++----- crates/engine/tests/stealth.rs | 56 ++++---- crates/engine/tests/tariswap.rs | 10 +- crates/engine/tests/template_upgrade.rs | 20 +-- crates/engine/tests/test.rs | 22 +-- crates/engine/tests/transaction_receipt.rs | 10 +- crates/engine/tests/validator_fees.rs | 6 +- .../account_balances/multi_vault_u64.json | 4 +- .../address_codec/identity_esmeralda.json | 4 +- .../identity_localnet_with_pay_ref.json | 4 +- .../address_codec/identity_mainnet.json | 4 +- .../address_codec/parse_component.json | 4 +- .../parse_identity_esmeralda.json | 4 +- .../address_codec/parse_resource.json | 4 +- .../address_derive/from_curve_pk.json | 4 +- .../address_derive/from_recipient_pk.json | 4 +- .../address_derive/from_seed_account_pk.json | 4 +- .../fixtures/arg_dsl/address_component.json | 4 +- .../arg_dsl/address_non_fungible.json | 4 +- .../fixtures/arg_dsl/address_resource.json | 4 +- .../fixtures/arg_dsl/address_template.json | 4 +- .../fixtures/arg_dsl/address_tombstone.json | 4 +- .../arg_dsl/address_transaction_receipt.json | 4 +- .../fixtures/arg_dsl/address_utxo.json | 4 +- .../arg_dsl/address_validator_fee_pool.json | 4 +- .../fixtures/arg_dsl/address_vault.json | 4 +- .../fixtures/arg_dsl/amount.json | 4 +- .../arg_dsl/amount_above_2_pow_33.json | 4 +- .../fixtures/arg_dsl/bool_false.json | 4 +- .../fixtures/arg_dsl/bool_true.json | 4 +- .../fixtures/arg_dsl/bytes.json | 4 +- .../fixtures/arg_dsl/i64_min.json | 4 +- .../fixtures/arg_dsl/i64_negative.json | 4 +- .../fixtures/arg_dsl/i64_positive.json | 4 +- .../fixtures/arg_dsl/list_empty.json | 4 +- .../fixtures/arg_dsl/list_nested.json | 4 +- .../fixtures/arg_dsl/list_of_addresses.json | 4 +- .../fixtures/arg_dsl/list_of_nfids.json | 4 +- .../fixtures/arg_dsl/metadata.json | 4 +- .../fixtures/arg_dsl/nfid_str.json | 4 +- .../fixtures/arg_dsl/nfid_u32.json | 4 +- .../fixtures/arg_dsl/nfid_u64.json | 4 +- .../fixtures/arg_dsl/nfid_uuid.json | 4 +- .../fixtures/arg_dsl/optional_address.json | 4 +- .../fixtures/arg_dsl/optional_none.json | 4 +- .../fixtures/arg_dsl/optional_some.json | 4 +- .../fixtures/arg_dsl/string.json | 4 +- .../ootle_sdk_core/fixtures/arg_dsl/u64.json | 4 +- .../fixtures/arg_dsl/u64_above_2_pow_33.json | 4 +- .../fixtures/cosign/seal_with_auth.json | 4 +- .../fixtures/generic_build/call_function.json | 8 +- .../generic_build/call_method_transfer.json | 4 +- .../generic_build/create_account.json | 8 +- .../fixtures/generic_build/faucet_claim.json | 8 +- .../generic_build/publish_template.json | 8 +- .../generic_build/self_funding_faucet.json | 8 +- .../generic_build/workspace_pipe.json | 8 +- .../fixtures/keys/account_from_seed.json | 4 +- .../fixtures/keys/view_from_seed.json | 4 +- .../parse_finalized_result/accept.json | 4 +- .../accept_fee_reject_rest.json | 4 +- .../parse_finalized_result/dry_run.json | 4 +- .../reject_epoch_expired.json | 4 +- .../public_transfer/large_amount.json | 4 +- .../sample_single_key_basic.json | 4 +- .../public_transfer/single_key_basic.json | 4 +- .../resolve_public_transfer/large_amount.json | 4 +- .../single_key_basic.json | 4 +- .../single_output_no_view_key.json | 6 +- .../single_output_with_view_key.json | 6 +- .../fixtures/stealth_scan/decode_utxo.json | 4 +- .../fixtures/stealth_scan/mine_basic.json | 4 +- .../fixtures/stealth_scan/not_mine.json | 4 +- .../account_key_seal_with_revealed_input.json | 8 +- .../revealed_output_multi.json | 8 +- .../revealed_output_single.json | 8 +- .../stealth_seal_with_input.json | 8 +- .../fixtures/substate_decode/component.json | 4 +- .../substate_decode/fungible_vault.json | 4 +- crates/ootle_sdk_core/src/builder.rs | 15 +- crates/ootle_sdk_core/src/cosign.rs | 2 +- crates/ootle_sdk_core/src/faucet.rs | 8 +- crates/ootle_sdk_core/src/generic_builder.rs | 30 ++-- crates/ootle_sdk_core/src/inputs.rs | 4 +- crates/ootle_sdk_core/src/public_transfer.rs | 2 +- .../ootle_sdk_core/src/resolved_transfer.rs | 2 +- crates/ootle_sdk_core/src/stealth/assemble.rs | 26 ++-- .../src/stealth/canonicalize.rs | 2 +- crates/ootle_sdk_core/src/stealth/inputs.rs | 4 +- crates/ootle_sdk_core/src/stealth/outputs.rs | 2 +- .../ootle_sdk_core/src/stealth/sign_seal.rs | 4 +- .../src/types/generic_intent.rs | 9 +- crates/ootle_sdk_core/src/types/intent.rs | 9 +- crates/ootle_sdk_core/src/types/stealth.rs | 9 +- crates/ootle_sdk_core/tests/golden_vectors.rs | 35 +++-- .../tests/ootle_rs_crosscheck.rs | 3 +- crates/ootle_wasm/core/src/hash.rs | 5 +- crates/ootle_wasm/core/src/transaction.rs | 3 +- crates/state_store_rocksdb/src/writer.rs | 2 +- .../state_store_rocksdb/tests/transactions.rs | 84 ++++++++--- .../src/consensus_models/transaction_pool.rs | 12 +- crates/storage/src/state_store/mod.rs | 2 +- .../template_builtin/tests/liquidity_pool.rs | 24 ++-- crates/template_builtin/tests/nft_faucet.rs | 4 +- crates/template_builtin/tests/xtr_faucet.rs | 4 +- .../src/template_test.rs | 2 +- crates/transaction/src/builder/mod.rs | 19 ++- crates/transaction/src/builder/tests.rs | 15 +- crates/transaction/src/lib.rs | 3 + crates/transaction/src/transaction.rs | 14 +- .../transaction/src/unsigned_transaction.rs | 8 +- crates/transaction/src/v1/intent.rs | 9 +- crates/transaction/src/v1/pruned.rs | 6 +- crates/transaction/src/v1/signature.rs | 16 +-- crates/transaction/src/v1/transaction.rs | 6 +- crates/transaction/src/v1/unsealed.rs | 2 +- crates/transaction/src/v1/unsigned.rs | 14 +- crates/transaction_validation/src/dry_run.rs | 3 +- .../transaction_validation/src/epoch_range.rs | 125 +++++++++++++++- crates/transaction_validation/src/error.rs | 17 ++- crates/transaction_validation/src/network.rs | 3 +- .../src/publish_template_limits.rs | 3 +- .../src/signature_limits.rs | 3 +- .../src/stealth_limits.rs | 3 +- crates/transaction_validation/src/weight.rs | 3 +- .../app/src/handlers/sign_transaction.rs | 16 ++- crates/wallet/ledger/app/src/state.rs | 2 +- .../ledger/client/tests/signing_recipe.rs | 3 +- .../wallet/ootle-rs/examples/adaptor_claim.rs | 10 +- crates/wallet/ootle-rs/examples/claim_burn.rs | 8 +- .../ootle-rs/examples/fungible_transfer.rs | 9 +- .../examples/stealth_spend_conditions.rs | 10 +- .../ootle-rs/examples/stealth_transfer.rs | 10 +- .../ootle-rs/examples/template_invoke.rs | 22 +-- .../ootle-rs/src/builtin_templates/account.rs | 8 +- .../src/builtin_templates/component.rs | 8 +- .../ootle-rs/src/builtin_templates/faucet.rs | 8 +- crates/wallet/ootle-rs/src/claim_burn/mod.rs | 16 ++- .../src/key_provider/local/generic_impls.rs | 4 +- crates/wallet/ootle-rs/src/macros.rs | 21 +-- crates/wallet/ootle-rs/src/wallet/stealth.rs | 3 +- .../sdk/src/apis/confidential_transfer.rs | 7 +- .../sdk/src/apis/stealth_transfer/api.rs | 4 +- .../sdk/src/apis/stealth_transfer/params.rs | 6 + .../sdk/src/models/transaction_request.rs | 2 +- .../wallet/sdk_tests/tests/balance_changes.rs | 2 +- .../storage_sqlite/tests/balance_changes.rs | 2 +- .../storage_sqlite/tests/transaction.rs | 4 +- .../tests/transaction_requests.rs | 6 +- integration_tests/src/util.rs | 10 +- .../src/validator_node_client.rs | 12 +- integration_tests/src/wallet_daemon_client.rs | 15 +- utilities/tariswap_test_bench/src/runner.rs | 13 +- utilities/traffic-sim/src/sim.rs | 19 ++- utilities/transaction_generator/src/cli.rs | 6 + utilities/transaction_generator/src/main.rs | 5 +- .../src/transaction_builders/free_coins.rs | 6 +- .../src/transaction_builders/manifest.rs | 5 +- .../tests/manifest_inputs.rs | 3 +- .../tests/max_compute.rs | 8 +- .../tests/publish_template.rs | 4 +- .../tests/publish_template_engine.rs | 3 +- 213 files changed, 1337 insertions(+), 855 deletions(-) diff --git a/applications/tari_indexer/src/storage_sqlite/store_factory.rs b/applications/tari_indexer/src/storage_sqlite/store_factory.rs index c1c70f61fb..c50dd3f2c5 100644 --- a/applications/tari_indexer/src/storage_sqlite/store_factory.rs +++ b/applications/tari_indexer/src/storage_sqlite/store_factory.rs @@ -333,7 +333,7 @@ mod tests { let (_dir, store) = temp_store().await; - let transaction = Transaction::builder_localnet().build_and_seal(&PrivateKey::from(123u64)); + let transaction = Transaction::builder_localnet(Epoch(1)).build_and_seal(&PrivateKey::from(123u64)); let tx_id = transaction.calculate_id(); store .with_write_tx(move |tx| tx.insert_or_ignore_transaction(&transaction)) diff --git a/applications/tari_validator_node/src/bootstrap.rs b/applications/tari_validator_node/src/bootstrap.rs index 048aa8b73e..bb4eb9a87e 100644 --- a/applications/tari_validator_node/src/bootstrap.rs +++ b/applications/tari_validator_node/src/bootstrap.rs @@ -361,7 +361,7 @@ pub async fn spawn_services( let transaction_executor = TariBlockTransactionExecutor::new(transaction_processor, consensus_constants.clone()); let transaction_validator = TariBlockTransactionValidator::new( create_mempool_transaction_validator(config.network, template_provider.clone()).boxed(), - EpochRangeValidator::new().boxed(), + EpochRangeValidator::new(consensus_constants.max_transaction_validity_epochs).boxed(), ); #[cfg(feature = "metrics")] @@ -397,6 +397,7 @@ pub async fn spawn_services( let (mempool, join_handle) = mempool::spawn( epoch_manager.clone(), create_mempool_transaction_validator(config.network, template_provider.clone()), + EpochRangeValidator::new(consensus_constants.max_transaction_validity_epochs), state_store.clone(), consensus_handle.clone(), networking.clone(), diff --git a/applications/tari_validator_node/src/p2p/services/mempool/initializer.rs b/applications/tari_validator_node/src/p2p/services/mempool/initializer.rs index 17a05c9aaf..f41e2245e9 100644 --- a/applications/tari_validator_node/src/p2p/services/mempool/initializer.rs +++ b/applications/tari_validator_node/src/p2p/services/mempool/initializer.rs @@ -23,6 +23,7 @@ use log::*; use tari_epoch_manager::service::EpochManagerHandle; use tari_networking::{GossipMessage, NetworkingHandle}; +use tari_ootle_common_types::Epoch; use tari_ootle_p2p::{PeerAddress, TariMessagingSpec}; use tari_ootle_storage::StateStore; use tari_ootle_transaction::Transaction; @@ -38,9 +39,10 @@ use crate::{ const LOG_TARGET: &str = "tari::ootle::validator_node::mempool"; -pub fn spawn( +pub fn spawn( epoch_manager: EpochManagerHandle, transaction_validator: TValidator, + epoch_validator: TEpochValidator, state_store: TStateStore, consensus_handle: ConsensusHandle, networking: NetworkingHandle, @@ -49,6 +51,8 @@ pub fn spawn( ) -> (MempoolHandle, JoinHandle>) where TValidator: Validator + Send + Sync + 'static, + TEpochValidator: + Validator + Send + Sync + 'static, TStateStore: StateStore + Send + Sync + 'static, { // This channel only needs to be size 1, because each mempool request must wait for a reply and the mempool is @@ -61,6 +65,7 @@ where rx_mempool_request, epoch_manager, transaction_validator, + epoch_validator, state_store, consensus_handle, networking, diff --git a/applications/tari_validator_node/src/p2p/services/mempool/service.rs b/applications/tari_validator_node/src/p2p/services/mempool/service.rs index 5c0fe2930e..d224c155c5 100644 --- a/applications/tari_validator_node/src/p2p/services/mempool/service.rs +++ b/applications/tari_validator_node/src/p2p/services/mempool/service.rs @@ -27,7 +27,7 @@ use log::*; use tari_consensus::hotstuff::HotstuffEvent; use tari_epoch_manager::{EpochManagerReader, service::EpochManagerHandle}; use tari_networking::{GossipMessage, NetworkingHandle}; -use tari_ootle_common_types::optional::Optional; +use tari_ootle_common_types::{Epoch, optional::Optional}; use tari_ootle_p2p::{NewTransactionMessage, PeerAddress, TariMessage, TariMessagingSpec}; use tari_ootle_storage::{StateStore, StateStoreReadTransaction, StorageError, consensus_models::TransactionRecord}; use tari_ootle_transaction::{Transaction, TransactionId}; @@ -53,11 +53,12 @@ const LOG_TARGET: &str = "tari::validator_node::mempool::service"; const MEM_MAX_TRANSACTIONS_DEDUP: usize = 1_000_000; #[derive(Debug)] -pub struct MempoolService { +pub struct MempoolService { transactions: SeenTransactions, mempool_requests: mpsc::Receiver, epoch_manager: EpochManagerHandle, before_execute_validator: TValidator, + epoch_validator: TEpochValidator, state_store: TStateStore, gossip: MempoolGossip, consensus_handle: ConsensusHandle, @@ -65,15 +66,17 @@ pub struct MempoolService { metrics: PrometheusMempoolMetrics, } -impl MempoolService +impl MempoolService where TValidator: Validator, + TEpochValidator: Validator, TStateStore: StateStore, { pub(super) fn new( mempool_requests: mpsc::Receiver, epoch_manager: EpochManagerHandle, before_execute_validator: TValidator, + epoch_validator: TEpochValidator, state_store: TStateStore, consensus_handle: ConsensusHandle, networking: NetworkingHandle, @@ -86,6 +89,7 @@ where mempool_requests, epoch_manager, before_execute_validator, + epoch_validator, state_store, consensus_handle, #[cfg(feature = "metrics")] @@ -263,7 +267,14 @@ where self.metrics.on_transaction_received(&transaction); let is_local = gossip_validation.is_none(); - let validation_result = self.before_execute_validator.validate(&(), &transaction); + // The epoch-dependent rules are checked here, alongside the structural ones, so that a + // transaction outside its validity window is refused before it is admitted or re-gossiped + // rather than after. Both feed the single acceptance verdict below. + let current_epoch = self.consensus_handle.current_view().get_epoch(); + let validation_result = self + .before_execute_validator + .validate(&(), &transaction) + .and_then(|_| self.epoch_validator.validate(¤t_epoch, &transaction)); // Reported here rather than at the end of this function: everything below is about whether // *we* act on the transaction, not whether it is valid, and gossipsub only holds a message @@ -290,8 +301,6 @@ where return Err(e.into()); } - let current_epoch = self.consensus_handle.current_view().get_epoch(); - let local_committee_shard = self.epoch_manager.get_local_committee_info(current_epoch).await?; let is_involved = transaction.is_involved(&local_committee_shard); diff --git a/applications/tari_wallet_cli/src/command/transaction.rs b/applications/tari_wallet_cli/src/command/transaction.rs index e04e47d217..2e2c9f2486 100644 --- a/applications/tari_wallet_cli/src/command/transaction.rs +++ b/applications/tari_wallet_cli/src/command/transaction.rs @@ -125,10 +125,21 @@ pub struct CommonSubmitArgs { pub fee_account: Option, #[clap(long)] pub min_epoch: Option, + /// The last epoch this transaction may be sequenced in. Defaults to the wallet daemon's + /// configured window past the current epoch. #[clap(long)] pub max_epoch: Option, } +impl CommonSubmitArgs { + /// The caller's explicit `max_epoch`, or the daemon's default window past the current epoch. + fn resolve_max_epoch(&self, current_epoch: Epoch, default_validity_epochs: u64) -> Epoch { + self.max_epoch + .map(Epoch) + .unwrap_or_else(|| Epoch(current_epoch.as_u64().saturating_add(default_validity_epochs))) + } +} + #[derive(Debug, Args, Clone)] pub struct SubmitManifestArgs { manifest: PathBuf, @@ -256,16 +267,21 @@ pub async fn handle_submit(args: SubmitArgs, client: &mut WalletDaemonClient) -> .owner_key_id .ok_or_else(|| anyhow!("Fee account does not have an owner key ID"))?; - let SettingsGetResponse { network, .. } = client.get_settings().await?; + let SettingsGetResponse { + network, + current_epoch, + default_transaction_validity_epochs, + .. + } = client.get_settings().await?; + let max_epoch = common.resolve_max_epoch(current_epoch, default_transaction_validity_epochs); - let mut builder = Transaction::builder(network.byte) + let mut builder = Transaction::builder(network.byte, max_epoch) .call_method(fee_account.component_address, "withdraw", args![common.max_fee,]) .put_last_instruction_output_on_workspace("fee_bucket") .pay_fee_from_bucket("fee_bucket") .add_instruction(instruction) .with_inputs(common.inputs) - .with_min_epoch(common.min_epoch.map(Epoch)) - .with_max_epoch(common.max_epoch.map(Epoch)); + .with_min_epoch(common.min_epoch.map(Epoch)); if let Some(dump_account) = common.dump_outputs_into { let AccountGetResponse { account, .. } = client.accounts_get(dump_account).await?; @@ -336,9 +352,15 @@ async fn handle_submit_manifest( .owner_key_id .ok_or_else(|| anyhow!("Fee account does not have an owner key ID"))?; - let SettingsGetResponse { network, .. } = client.get_settings().await?; + let SettingsGetResponse { + network, + current_epoch, + default_transaction_validity_epochs, + .. + } = client.get_settings().await?; + let max_epoch = common.resolve_max_epoch(current_epoch, default_transaction_validity_epochs); - let builder = Transaction::builder(network.byte) + let builder = Transaction::builder(network.byte, max_epoch) .with_fee_instructions_builder(|builder| { builder.with_instructions(instructions.fee_instructions).call_method( fee_account.component_address, @@ -348,8 +370,7 @@ async fn handle_submit_manifest( }) .with_instructions(instructions.instructions) .with_inputs(common.inputs) - .with_min_epoch(common.min_epoch.map(Epoch)) - .with_max_epoch(common.max_epoch.map(Epoch)); + .with_min_epoch(common.min_epoch.map(Epoch)); let transaction = builder.build_unsigned(); summarize_transaction(&transaction); diff --git a/applications/tari_walletd/examples/transaction_request_flow.rs b/applications/tari_walletd/examples/transaction_request_flow.rs index a3f65a9443..1967133484 100644 --- a/applications/tari_walletd/examples/transaction_request_flow.rs +++ b/applications/tari_walletd/examples/transaction_request_flow.rs @@ -35,7 +35,7 @@ use std::time::Duration; use anyhow::{Context, anyhow, bail}; use clap::Parser; use tari_engine_types::component::derive_component_address_from_public_key; -use tari_ootle_transaction::{TransactionBuilder, UnsignedTransaction, args}; +use tari_ootle_transaction::{Epoch, TransactionBuilder, UnsignedTransaction, args}; use tari_ootle_wallet_sdk::models::EffectiveStatus; use tari_ootle_walletd_client::{ WalletDaemonClient, @@ -225,7 +225,7 @@ fn build_transfer( max_fee: u64, ) -> UnsignedTransaction { let dest = derive_component_address_from_public_key(&ACCOUNT_TEMPLATE_ADDRESS, &dest_pk); - TransactionBuilder::new(network) + TransactionBuilder::new(network, Epoch(100)) .create_account(dest_pk) .pay_fee_from_component(source, Amount::from_integer(max_fee)) .call_method(source, "withdraw", args![TARI_TOKEN, Amount::from_integer(amount)]) diff --git a/applications/tari_walletd/src/config.rs b/applications/tari_walletd/src/config.rs index bfed48bafb..5328c4a9db 100644 --- a/applications/tari_walletd/src/config.rs +++ b/applications/tari_walletd/src/config.rs @@ -102,6 +102,14 @@ pub struct WalletDaemonConfig { /// transactions when new burn proof files are detected, deferring each claim to the next epoch as required. #[serde(default = "return_default_auto_claim_burns")] pub auto_claim_burns: bool, + /// How many epochs ahead of the current epoch to stamp `max_epoch` on transactions this wallet + /// builds, when the caller does not supply one. Every transaction must declare the last epoch it + /// may be sequenced in; past it the transaction can never land, so this decides how long a built + /// transaction stays submittable. Must not exceed the network's `max_transaction_validity_epochs` + /// ceiling — the default is far below it, leaving the long windows to callers that ask for one + /// explicitly (e.g. offline or multi-party signing). + #[serde(default = "return_default_transaction_validity_epochs")] + pub default_transaction_validity_epochs: u64, } impl WalletDaemonConfig { @@ -116,6 +124,12 @@ fn return_default_auto_claim_burns() -> bool { true } +/// Three epochs is roughly an hour at the ~20 minute epoch target: long enough for an interactive +/// approval flow, short enough that an abandoned transaction stops being submittable quickly. +fn return_default_transaction_validity_epochs() -> u64 { + 3 +} + fn return_default_transaction_request_ttl() -> Duration { Duration::from_secs(30 * 60) } @@ -153,6 +167,7 @@ impl Default for WalletDaemonConfig { burn_proof_dir: None, override_keyring_password: None, auto_claim_burns: true, + default_transaction_validity_epochs: return_default_transaction_validity_epochs(), } } } diff --git a/applications/tari_walletd/src/handlers/accounts.rs b/applications/tari_walletd/src/handlers/accounts.rs index ab9e7b4489..071feebc9f 100644 --- a/applications/tari_walletd/src/handlers/accounts.rs +++ b/applications/tari_walletd/src/handlers/accounts.rs @@ -15,7 +15,7 @@ use tari_engine_types::{ confidential::ClaimBurnOutputData, substate::SubstateId, }; -use tari_ootle_common_types::{SubstateRequirement, optional::Optional}; +use tari_ootle_common_types::{Epoch, SubstateRequirement, optional::Optional}; use tari_ootle_transaction::{Transaction, args}; use tari_ootle_wallet_crypto::{ OutputWitness, @@ -528,6 +528,7 @@ pub async fn handle_claim_burn( &account, proof_contents, max_fee, + context.transaction_max_epoch().await?, is_dry_run, proof_file_name, ) @@ -544,6 +545,7 @@ pub(crate) async fn execute_claim_burn( account: &AccountWithAddress, proof_contents: ClaimBurnProofContents, max_fee: u64, + max_epoch: Epoch, is_dry_run: bool, proof_file_name: Option, ) -> Result { @@ -667,7 +669,7 @@ pub(crate) async fn execute_claim_burn( encrypted_data: claimed_encrypted_data, }; - let transaction = Transaction::builder(network.as_byte()) + let transaction = Transaction::builder(network.as_byte(), max_epoch) .with_fee_instructions_builder(|fee_builder| { fee_builder // Mint the UTXO @@ -814,6 +816,7 @@ pub async fn handle_create_free_test_coins( let transaction = context .transaction_builder() + .await? .with_fee_instructions_builder(|fee_builder| { fee_builder .create_account(*account.address.account_public_key()) @@ -937,7 +940,10 @@ pub async fn handle_transfer( .await .optional()?; - let builder = context.transaction_builder().create_account(req.destination_public_key); + let builder = context + .transaction_builder() + .await? + .create_account(req.destination_public_key); if let Some(ValidatorScanResult { id: address, substate }) = existing_dest_account { inputs.insert(address.into()); @@ -1101,6 +1107,7 @@ pub async fn handle_confidential_transfer( )])?; let transaction_service = context.transaction_service().clone(); + let max_epoch = context.transaction_max_epoch().await?; // Spawn here is to prevent the async block from being aborted if the caller aborts the request early as this can // cause funds to remain locked indefinitely. @@ -1117,6 +1124,7 @@ pub async fn handle_confidential_transfer( let transfer = sdk .confidential_transfer_api() .transfer(ConfidentialTransferParams { + max_epoch, from_account: source_account_address, input_selection: req.input_selection, amount: req.amount, @@ -1276,6 +1284,7 @@ pub async fn handle_stealth_transfer( .collect::>()?; let params = StealthTransferParams { + max_epoch: context.transaction_max_epoch().await?, fee_params: req.fee_params, input_selection: req.input_selection, resource_address: req.resource_address, diff --git a/applications/tari_walletd/src/handlers/context.rs b/applications/tari_walletd/src/handlers/context.rs index f80ce625c7..eb70587713 100644 --- a/applications/tari_walletd/src/handlers/context.rs +++ b/applications/tari_walletd/src/handlers/context.rs @@ -2,14 +2,14 @@ // SPDX-License-Identifier: BSD-3-Clause use std::{ - sync::Arc, + sync::{Arc, Mutex}, time::{Duration, Instant}, }; use axum_extra::headers::authorization::Bearer; use dashmap::DashMap; -use tari_ootle_transaction::{Transaction, TransactionBuilder}; -use tari_ootle_wallet_sdk::models::WalletEvent; +use tari_ootle_transaction::{Epoch, Transaction, TransactionBuilder}; +use tari_ootle_wallet_sdk::{models::WalletEvent, network::WalletNetworkInterface}; use tari_ootle_wallet_sdk_services::{ account_monitor::AccountMonitorHandle, notify::Notify, @@ -59,8 +59,16 @@ pub struct HandlerContext { /// throttle in `api_key_touch_last_used` stays as belt-and-braces /// against process restart and racing shim invocations. api_key_last_used_bumps: Arc>, + /// Last epoch read from the network, with the time it was read. Every transaction build needs + /// the current epoch to stamp `max_epoch`; epochs turn over on the order of tens of minutes, so + /// a short cache keeps a burst of builds from making an indexer round-trip each. + cached_epoch: Arc>>, } +/// How long a read of the current epoch is reused before the network is asked again. Well under an +/// epoch, so the stamped window is never short by more than this. +const EPOCH_CACHE_TTL: Duration = Duration::from_secs(30); + impl HandlerContext { pub fn new( wallet_sdk: WalletSdk, @@ -83,6 +91,7 @@ impl HandlerContext { jwt_secret, shutdown_signal, api_key_last_used_bumps: Arc::new(DashMap::new()), + cached_epoch: Arc::new(Mutex::new(None)), } } @@ -286,6 +295,30 @@ impl HandlerContext { self.authenticator.webauthn() } + /// The current epoch, re-read from the network at most every [`EPOCH_CACHE_TTL`]. + pub async fn current_epoch(&self) -> Result { + if let Some((epoch, read_at)) = *self.cached_epoch.lock().unwrap() && + read_at.elapsed() < EPOCH_CACHE_TTL + { + return Ok(epoch); + } + + let epoch = self.wallet_sdk.get_network_interface().get_current_epoch().await?; + *self.cached_epoch.lock().unwrap() = Some((epoch, Instant::now())); + Ok(epoch) + } + + /// The `max_epoch` this wallet stamps on transactions it builds: the current epoch plus the + /// configured validity window. + pub async fn transaction_max_epoch(&self) -> Result { + let current_epoch = self.current_epoch().await?; + Ok(Epoch( + current_epoch + .as_u64() + .saturating_add(self.config().default_transaction_validity_epochs), + )) + } + /// Returns a TransactionBuilder with the current network configured. /// /// The builder is stamped with a random nonce so that repeated identical intents (same @@ -293,7 +326,13 @@ impl HandlerContext { /// caller-provided bytes replace the whole unsigned transaction via /// `with_unsigned_transaction`, which discards the stamp — the caller's bytes are preserved /// verbatim there. - pub fn transaction_builder(&self) -> TransactionBuilder { - Transaction::builder(self.config().network.as_byte()).with_nonce(rand::random()) + /// + /// `max_epoch` is stamped `default_transaction_validity_epochs` ahead of the current epoch. + /// Building therefore depends on the network being reachable: without a current epoch there is + /// no way to choose a window the network will accept, so this fails rather than guessing. + /// Callers that want a different window override it with `with_max_epoch`. + pub async fn transaction_builder(&self) -> Result { + let max_epoch = self.transaction_max_epoch().await?; + Ok(Transaction::builder(self.config().network.as_byte(), max_epoch).with_nonce(rand::random())) } } diff --git a/applications/tari_walletd/src/handlers/nfts.rs b/applications/tari_walletd/src/handlers/nfts.rs index fbab3731d2..c551557310 100644 --- a/applications/tari_walletd/src/handlers/nfts.rs +++ b/applications/tari_walletd/src/handlers/nfts.rs @@ -119,6 +119,7 @@ pub async fn handle_mint_faucet( let fee = req.max_fee.unwrap_or(3000); let transaction = context .transaction_builder() + .await? .pay_fee_from_component(account.component_address, fee) .call_method(NFT_FAUCET_COMPONENT_ADDRESS, "mint", args![ req.number_to_mint, @@ -274,7 +275,7 @@ pub async fn handle_transfer( ); // TODO: this can be simplified - let mut builder = context.transaction_builder(); + let mut builder = context.transaction_builder().await?; // collect all instructions let non_fungible_api = sdk.non_fungible_api(); diff --git a/applications/tari_walletd/src/handlers/settings.rs b/applications/tari_walletd/src/handlers/settings.rs index bb1af72d59..c80fc184b5 100644 --- a/applications/tari_walletd/src/handlers/settings.rs +++ b/applications/tari_walletd/src/handlers/settings.rs @@ -35,6 +35,8 @@ pub async fn handle_get( .optional()? .unwrap_or_default(); + let current_epoch = context.current_epoch().await?; + Ok(SettingsGetResponse { indexer_url, network: NetworkInfo { @@ -43,6 +45,8 @@ pub async fn handle_get( }, advanced_ui_features, claimed_accounts, + current_epoch, + default_transaction_validity_epochs: context.config().default_transaction_validity_epochs, }) } diff --git a/applications/tari_walletd/src/handlers/transaction.rs b/applications/tari_walletd/src/handlers/transaction.rs index 4172cd2ddf..61fe62e00a 100644 --- a/applications/tari_walletd/src/handlers/transaction.rs +++ b/applications/tari_walletd/src/handlers/transaction.rs @@ -83,10 +83,14 @@ pub async fn handle_submit_instruction( })?; let transaction = context .transaction_builder() + .await? .with_instructions(req.instructions) .pay_fee_from_component(*fee_account.component_address(), req.max_fee.max(1)) .with_min_epoch(req.min_epoch.map(Epoch)) - .with_max_epoch(req.max_epoch.map(Epoch)) + .then(|b| match req.max_epoch { + Some(max_epoch) => b.with_max_epoch(Epoch(max_epoch)), + None => b, + }) .with_inputs(req.inputs) .build_unsigned(); @@ -185,6 +189,7 @@ async fn submit_inner( // signatures are added on top and the seal signature commits to all of them. let mut transaction = context .transaction_builder() + .await? .with_unsigned_transaction(req.transaction) .with_inputs(detected_inputs) .with_signatures(req.signatures); @@ -380,6 +385,7 @@ pub async fn handle_detect_inputs( let transaction = context .transaction_builder() + .await? .with_unsigned_transaction(req.transaction) .with_inputs(detected) .build_unsigned(); @@ -431,6 +437,7 @@ async fn submit_dry_run_inner( let mut transaction = context .transaction_builder() + .await? .with_unsigned_transaction(req.transaction) .with_inputs(detected_inputs) .with_dry_run(true) @@ -513,6 +520,7 @@ pub async fn handle_submit_manifest( let mut transaction = context .transaction_builder() + .await? .with_dry_run(req.dry_run) .with_fee_instructions_builder(|builder| { if instructions.fee_instructions.is_empty() { @@ -761,6 +769,7 @@ pub async fn handle_publish_template( let builder = context .transaction_builder() + .await? .pay_fee_from_component(*fee_account.component_address(), max_fee); let builder = match metadata_hash { Some(hash) => builder.publish_template_with_metadata(wasm_binary, hash), diff --git a/applications/tari_walletd/src/handlers/validator.rs b/applications/tari_walletd/src/handlers/validator.rs index 9ae96e08b3..c919e3329f 100644 --- a/applications/tari_walletd/src/handlers/validator.rs +++ b/applications/tari_walletd/src/handlers/validator.rs @@ -144,7 +144,7 @@ pub async fn handle_claim_validator_fees( let max_fee = req.max_fee.max(1); let account_public_key = *account.address.account_public_key(); - let builder = context.transaction_builder().with_dry_run(req.dry_run); + let builder = context.transaction_builder().await?.with_dry_run(req.dry_run); let builder = if req.output_to_revealed { let (first, rest) = fee_pool_addresses diff --git a/applications/tari_walletd/src/services/auto_claim_burn_service.rs b/applications/tari_walletd/src/services/auto_claim_burn_service.rs index e2f9cf03aa..316abc2bb3 100644 --- a/applications/tari_walletd/src/services/auto_claim_burn_service.rs +++ b/applications/tari_walletd/src/services/auto_claim_burn_service.rs @@ -32,6 +32,10 @@ use crate::{ const LOG_TARGET: &str = "tari::ootle::wallet_daemon::auto_claim_burn"; const EPOCH_CHECK_INTERVAL: Duration = Duration::from_secs(30); /// Maximum retries for network/submission errors (indexer unreachable, tx service down). +/// Validity window for an unattended claim: a few epochs is ample for the submit itself, and a +/// claim that does not land in that time is retried with a fresh window. +const CLAIM_TRANSACTION_VALIDITY_EPOCHS: u64 = 3; + const MAX_RETRIES_NETWORK: u32 = 10; /// Maximum retries for file read/parse errors (file still being written on macOS). const MAX_RETRIES_FILE_READ: u32 = 1; @@ -458,6 +462,7 @@ impl AutoClaimBurnService { &account, proof_contents.clone(), 1, + self.claim_max_epoch().await.map_err(ClaimError::Permanent)?, true, Some(file_name.to_string()), ) @@ -500,6 +505,7 @@ impl AutoClaimBurnService { &account, proof_contents, required_fees, + self.claim_max_epoch().await.map_err(ClaimError::Permanent)?, false, Some(file_name.to_string()), ) @@ -509,6 +515,15 @@ impl AutoClaimBurnService { Ok(resp.transaction_id) } + /// The validity window stamped on claim transactions this service builds. Claims are submitted + /// unattended, so the window only has to cover the submission itself. + async fn claim_max_epoch(&self) -> anyhow::Result { + let current = self.query_current_epoch().await?; + Ok(Epoch( + current.as_u64().saturating_add(CLAIM_TRANSACTION_VALIDITY_EPOCHS), + )) + } + async fn query_current_epoch(&self) -> anyhow::Result { self.sdk .get_network_interface() diff --git a/bindings/src/types/PrunedUnsignedTransactionV1.ts b/bindings/src/types/PrunedUnsignedTransactionV1.ts index d9bd63c5cd..c87a78f630 100644 --- a/bindings/src/types/PrunedUnsignedTransactionV1.ts +++ b/bindings/src/types/PrunedUnsignedTransactionV1.ts @@ -16,7 +16,7 @@ export type PrunedUnsignedTransactionV1 = { instructions: Array; inputs: Array; min_epoch: Epoch | null; - max_epoch: Epoch | null; + max_epoch: Epoch; is_seal_signer_authorized: boolean; dry_run: boolean; /** diff --git a/bindings/src/types/TransactionPoolRecord.ts b/bindings/src/types/TransactionPoolRecord.ts index 10a676b172..611e1c85b5 100644 --- a/bindings/src/types/TransactionPoolRecord.ts +++ b/bindings/src/types/TransactionPoolRecord.ts @@ -22,7 +22,7 @@ export type TransactionPoolRecord = { /** * The maximum epoch for which this transaction is valid. */ - max_epoch: Epoch | null; + max_epoch: Epoch; /** * Epoch to use when executing the transaction. This updates as foreign proposals are received * until the transaction is executed. diff --git a/bindings/src/types/UnsignedTransactionV1.ts b/bindings/src/types/UnsignedTransactionV1.ts index 647d894467..c14553c11d 100644 --- a/bindings/src/types/UnsignedTransactionV1.ts +++ b/bindings/src/types/UnsignedTransactionV1.ts @@ -13,7 +13,7 @@ export type UnsignedTransactionV1 = { */ inputs: Array; min_epoch: Epoch | null; - max_epoch: Epoch | null; + max_epoch: Epoch; is_seal_signer_authorized: boolean; dry_run: boolean; /** diff --git a/bindings/src/types/wallet-types/InputSelection.ts b/bindings/src/types/wallet-types/InputSelection.ts index 31c3c3bbf8..31c9a78e84 100644 --- a/bindings/src/types/wallet-types/InputSelection.ts +++ b/bindings/src/types/wallet-types/InputSelection.ts @@ -3,4 +3,7 @@ import type { Amount } from "../Amount"; import type { UtxoAddress } from "../UtxoAddress"; import type { UtxoInputSelection } from "../UtxoInputSelection"; -export type InputSelection = { "FromBucket": { revealed_amount: Amount, } } | { "Selection": UtxoInputSelection } | { "Specific": { utxo_addresses: Array, } }; +export type InputSelection = + | { FromBucket: { revealed_amount: Amount } } + | { Selection: UtxoInputSelection } + | { Specific: { utxo_addresses: Array } }; diff --git a/bindings/src/types/wallet-types/SettingsGetResponse.ts b/bindings/src/types/wallet-types/SettingsGetResponse.ts index 1e409b32cc..7351e0f568 100644 --- a/bindings/src/types/wallet-types/SettingsGetResponse.ts +++ b/bindings/src/types/wallet-types/SettingsGetResponse.ts @@ -7,4 +7,6 @@ export type SettingsGetResponse = { network: NetworkInfo; advanced_ui_features: AdvancedUiFeatures; claimed_accounts: Array; + current_epoch: number; + default_transaction_validity_epochs: number; }; diff --git a/clients/wallet_daemon_client/src/types.rs b/clients/wallet_daemon_client/src/types.rs index 0cc7dd6b85..c7fb5ae8e7 100644 --- a/clients/wallet_daemon_client/src/types.rs +++ b/clients/wallet_daemon_client/src/types.rs @@ -32,6 +32,7 @@ use tari_engine_types::{ }; use tari_ootle_address::OotleAddress; use tari_ootle_common_types::{ + Epoch, ShardGroup, SubstateAddress, SubstateRequirement, @@ -1335,6 +1336,14 @@ pub struct SettingsGetResponse { pub network: NetworkInfo, pub advanced_ui_features: AdvancedUiFeatures, pub claimed_accounts: Vec, + /// The network's current epoch, as the wallet daemon sees it. Callers that build transactions + /// themselves need it to choose a `max_epoch` the network will accept. + #[cfg_attr(feature = "ts", ts(type = "number"))] + pub current_epoch: Epoch, + /// How many epochs past `current_epoch` the wallet daemon stamps `max_epoch` when a caller does + /// not choose one. + #[cfg_attr(feature = "ts", ts(type = "number"))] + pub default_transaction_validity_epochs: u64, } #[derive(Debug, Clone, Default, Deserialize, Serialize)] diff --git a/crates/consensus/src/consensus_constants.rs b/crates/consensus/src/consensus_constants.rs index 95c2ed6d20..7821cd77fa 100644 --- a/crates/consensus/src/consensus_constants.rs +++ b/crates/consensus/src/consensus_constants.rs @@ -98,6 +98,16 @@ pub struct ConsensusConstants { /// totals in block headers. Use `exhaust_burn_rate` to resolve the rate for a given epoch rather than reading /// this field directly. pub exhaust_burn_rate_bps: u16, + /// The furthest ahead of the current epoch a transaction's `max_epoch` may be set. Every + /// transaction declares a mandatory `max_epoch`, so this caps how long any transaction can + /// remain sequenceable: a wallet can declare a transaction permanently dead once this many + /// epochs have passed, and an aborted attempt — which consensus deliberately allows to be + /// re-sequenced — cannot be retried beyond its window. This is a ceiling, not a default — + /// wallets stamp a much shorter window for ordinary traffic and only long-lived flows + /// (offline or multi-party signing) approach it. Enforced at mempool admission and in + /// consensus sequencing. CONSENSUS RULE: must be uniform network-wide, otherwise nodes + /// diverge on which transactions may be sequenced. + pub max_transaction_validity_epochs: u64, /// Number of base-layer blocks of leeway a voter is allowed when accepting `EndEpoch` proposals. /// If the voter's oracle has not yet crossed the next epoch boundary but its lagged scan height /// is within this many blocks of the boundary, the voter accepts `EndEpoch` from peers whose @@ -143,6 +153,7 @@ impl ConsensusConstants { // proposals are never rejected. max_block_validation_execution_points: 7_100_000_000, exhaust_burn_rate_bps: 500, // 5% + max_transaction_validity_epochs: 2160, epoch_end_spread_blocks: 10, } } @@ -183,6 +194,7 @@ impl ConsensusConstants { // proposals are never rejected. max_block_validation_execution_points: 7_100_000_000, exhaust_burn_rate_bps: 500, // 5% + max_transaction_validity_epochs: 2160, epoch_end_spread_blocks: 1, } } @@ -223,6 +235,7 @@ impl ConsensusConstants { // proposals are never rejected. max_block_validation_execution_points: 7_100_000_000, exhaust_burn_rate_bps: 500, // 5% + max_transaction_validity_epochs: 2160, epoch_end_spread_blocks: 5, } } @@ -263,6 +276,7 @@ impl ConsensusConstants { // proposals are never rejected. max_block_validation_execution_points: 7_100_000_000, exhaust_burn_rate_bps: 500, // 5% + max_transaction_validity_epochs: 2160, epoch_end_spread_blocks: 5, } } @@ -302,6 +316,23 @@ mod tests { use super::*; + /// A consensus rule must be identical on every network, otherwise a transaction admitted on one + /// is refused on another and nodes diverge on which transactions may be sequenced. + #[test] + fn the_transaction_validity_ceiling_is_uniform_across_networks() { + let expected = ConsensusConstants::mainnet().max_transaction_validity_epochs; + for constants in [ + ConsensusConstants::devnet(7), + ConsensusConstants::esmeralda(), + ConsensusConstants::testnet(), + ] { + assert_eq!(constants.max_transaction_validity_epochs, expected); + } + // A zero ceiling would admit only transactions expiring in the current epoch, leaving no + // room to submit one at all. + assert!(expected > 0); + } + #[test] fn validation_budgets_always_admit_honest_proposals() { for constants in [ diff --git a/crates/consensus/src/hotstuff/transaction_manager/manager.rs b/crates/consensus/src/hotstuff/transaction_manager/manager.rs index 29d319558d..115db33968 100644 --- a/crates/consensus/src/hotstuff/transaction_manager/manager.rs +++ b/crates/consensus/src/hotstuff/transaction_manager/manager.rs @@ -221,9 +221,8 @@ impl> pledged_transaction: PledgedTransaction, ) -> Result { // Abort before execution if the execution epoch exceeds the transaction's max_epoch - if let Some(max_epoch) = pledged_transaction.transaction.transaction().max_epoch() && - execution_epoch.epoch() > max_epoch - { + let max_epoch = pledged_transaction.transaction.transaction().max_epoch(); + if execution_epoch.epoch() > max_epoch { warn!( target: LOG_TARGET, "⏰ Transaction {} has expired: execution epoch {} exceeds max_epoch {}", @@ -270,9 +269,8 @@ impl> execution_locked_epoch: LockedEpoch, ) -> Result { // Abort before execution if the execution epoch exceeds the transaction's max_epoch - if let Some(max_epoch) = transaction.transaction().max_epoch() && - execution_locked_epoch.epoch() > max_epoch - { + let max_epoch = transaction.transaction().max_epoch(); + if execution_locked_epoch.epoch() > max_epoch { warn!( target: LOG_TARGET, "⏰ Transaction {} has expired: execution epoch {} exceeds max_epoch {}", diff --git a/crates/consensus_tests/src/consensus.rs b/crates/consensus_tests/src/consensus.rs index 184d11d7c8..698b7b61f9 100644 --- a/crates/consensus_tests/src/consensus.rs +++ b/crates/consensus_tests/src/consensus.rs @@ -705,7 +705,7 @@ async fn multishard_local_inputs_foreign_outputs() { let outputs_2 = test.build_outputs_for_committee(2, 1); let tx1 = build_transaction_from( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_inputs(inputs.iter().cloned().map(|i| i.into())) .build_and_seal(&PrivateKey::default()), ); @@ -762,7 +762,7 @@ async fn multishard_local_inputs_foreign_outputs_abort() { let inputs = test.create_substates_on_vns(TestVnDestination::Committee(0), 2); let outputs = test.build_outputs_for_committee(1, 1); - let transaction = Transaction::builder_localnet() + let transaction = Transaction::builder_localnet(Epoch(1)) .with_inputs(inputs.iter().cloned().map(|i| i.into())) .build_and_seal(&PrivateKey::default()); @@ -842,7 +842,7 @@ async fn multishard_local_inputs_and_outputs_foreign_outputs() { let outputs_2 = test.build_outputs_for_committee(2, 5); let tx1 = build_transaction_from( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_inputs(inputs_0.iter().chain(&inputs_1).cloned().map(|i| i.into())) .build_and_seal(&PrivateKey::from_canonical_bytes(&[1; 32]).unwrap()), ); @@ -919,7 +919,7 @@ async fn multishard_output_conflict_abort() { .await; let inputs = test.create_substates_on_vns(TestVnDestination::All, 1); - let tx = Transaction::builder_localnet() + let tx = Transaction::builder_localnet(Epoch(1)) .with_inputs(inputs.iter().cloned().map(|i| i.into())) .build_and_seal(&Default::default()); let tx2 = build_transaction_from(tx); @@ -1006,7 +1006,7 @@ async fn single_shard_inputs_from_previous_outputs() { .map(|output| SubstateRequirement::versioned(output.clone(), 0)) .collect::>(); - let tx2 = Transaction::builder_localnet() + let tx2 = Transaction::builder_localnet(Epoch(1)) .with_inputs(prev_outputs.clone()) .build_and_seal(&Default::default()); let tx2 = build_transaction_from(tx2.clone()); @@ -1070,7 +1070,7 @@ async fn multishard_inputs_from_previous_outputs() { .map(|output| SubstateRequirement::versioned(output.clone(), 0)) .collect::>(); - let tx2 = Transaction::builder_localnet() + let tx2 = Transaction::builder_localnet(Epoch(1)) .with_inputs(prev_outputs.clone()) .build_and_seal(&Default::default()); let tx2 = build_transaction_from(tx2.clone()); @@ -1130,12 +1130,12 @@ async fn single_shard_input_conflict() { let secret1 = PrivateKey::from_canonical_bytes(&[1u8; 32]).unwrap(); let secret2 = PrivateKey::from_canonical_bytes(&[2u8; 32]).unwrap(); - let tx1 = Transaction::builder_localnet() + let tx1 = Transaction::builder_localnet(Epoch(1)) .add_input(substate_id.clone()) .build_and_seal(&secret1); let tx1 = TransactionRecord::new(tx1); - let tx2 = Transaction::builder_localnet() + let tx2 = Transaction::builder_localnet(Epoch(1)) .add_input(substate_id.clone()) .build_and_seal(&secret2); let tx2 = TransactionRecord::new(tx2); @@ -1267,7 +1267,7 @@ async fn single_shard_unversioned_inputs() { let unversioned_inputs = inputs .iter() .map(|i| SubstateRequirement::new(i.substate_id().clone(), None)); - let tx = Transaction::builder_localnet() + let tx = Transaction::builder_localnet(Epoch(1)) .with_inputs(unversioned_inputs) .build_and_seal(&PrivateKey::default()); let tx = TransactionRecord::new(tx); @@ -1349,13 +1349,13 @@ async fn multishard_unversioned_input_conflict() { // Distinct sealers: the transaction id excludes the seal nonce, so an identical body sealed // by the same key would be one transaction, not two conflicting ones. - let tx1 = Transaction::builder_localnet() + let tx1 = Transaction::builder_localnet(Epoch(1)) .add_input(SubstateRequirement::unversioned(id0.substate_id().clone())) .add_input(SubstateRequirement::unversioned(id1.substate_id().clone())) .build_and_seal(&PrivateKey::from_canonical_bytes(&[1u8; 32]).unwrap()); let tx1 = TransactionRecord::new(tx1); - let tx2 = Transaction::builder_localnet() + let tx2 = Transaction::builder_localnet(Epoch(1)) .add_input(SubstateRequirement::unversioned(id0.substate_id().clone())) .add_input(SubstateRequirement::unversioned(id1.substate_id().clone())) .build_and_seal(&PrivateKey::from_canonical_bytes(&[2u8; 32]).unwrap()); @@ -1453,13 +1453,13 @@ async fn multishard_unversioned_input_conflict_delay_prepare() { .pop() .unwrap(); - let tx1 = Transaction::builder_localnet() + let tx1 = Transaction::builder_localnet(Epoch(1)) .add_input(SubstateRequirement::unversioned(id0.substate_id().clone())) .add_input(SubstateRequirement::unversioned(id1.substate_id().clone())) .build_and_seal(&Default::default()); let tx1 = TransactionRecord::new(tx1); - let tx2 = Transaction::builder_localnet() + let tx2 = Transaction::builder_localnet(Epoch(1)) .add_input(SubstateRequirement::unversioned(id0.substate_id().clone())) .add_input(SubstateRequirement::unversioned(id2.substate_id().clone())) .build_and_seal(&Default::default()); @@ -1543,7 +1543,7 @@ async fn multishard_publish_template() { let (sk, pk) = create_key_pair(); let wasm = load_binary_fixture("state.wasm"); let expected_binary_hash = hash_template_code(&wasm); - let tx = Transaction::builder_localnet() + let tx = Transaction::builder_localnet(Epoch(1)) .publish_template(wasm) .with_inputs(inputs.iter().cloned().map(Into::into)) .build_and_seal(&sk); @@ -1614,7 +1614,7 @@ async fn multishard_validator_fee_claim() { .await; // Create and send publish template transaction let address = derive_fee_pool_address(&claim_bytes, test.num_preshards(), Shard::first()); - let claim_tx = Transaction::builder_localnet() + let claim_tx = Transaction::builder_localnet(Epoch(1)) .claim_validator_fees(address) .add_input(address) .build_and_seal(&claim_sk); @@ -1751,9 +1751,9 @@ async fn single_transaction_epoch_expired() { let inputs = test.create_substates_on_vns(TestVnDestination::All, 1); let tx = build_transaction_from( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_inputs(inputs.iter().cloned().map(|i| i.into())) - .with_max_epoch(Some(Epoch(0))) + .with_max_epoch(Epoch(0)) .call_function(Default::default(), "foo", args![]) .build_and_seal(&PrivateKey::default()), ); @@ -1805,9 +1805,9 @@ async fn multishard_transaction_epoch_expired() { let inputs = test.create_substates_on_vns(TestVnDestination::Committee(0), 2); let outputs = test.build_outputs_for_committee(1, 1); let tx = build_transaction_from( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_inputs(inputs.iter().cloned().map(|i| i.into())) - .with_max_epoch(Some(Epoch(0))) + .with_max_epoch(Epoch(0)) .call_function(Default::default(), "foo", args![]) .build_and_seal(&PrivateKey::default()), ); diff --git a/crates/consensus_tests/src/support/harness.rs b/crates/consensus_tests/src/support/harness.rs index b08f9f543c..b3cfbb7f31 100644 --- a/crates/consensus_tests/src/support/harness.rs +++ b/crates/consensus_tests/src/support/harness.rs @@ -667,6 +667,7 @@ impl TestBuilder { missed_proposal_suspend_threshold: 5, missed_proposal_evict_threshold: 10, missed_proposal_recovery_threshold: 5, + max_transaction_validity_epochs: 100, // Keep the weight budget effectively unbounded in tests so behaviour stays // count-limited (as before) unless a test specifically exercises the weight budget. max_block_weight: 1_000_000, diff --git a/crates/consensus_tests/src/support/transaction.rs b/crates/consensus_tests/src/support/transaction.rs index 825a04b112..8d8cd7c3ef 100644 --- a/crates/consensus_tests/src/support/transaction.rs +++ b/crates/consensus_tests/src/support/transaction.rs @@ -187,7 +187,7 @@ pub fn random_substates_ids_for_committee_generator( pub fn build_transaction(inputs: Vec) -> TransactionRecord { let k = PrivateKey::default(); - let tx = Transaction::builder_localnet() + let tx = Transaction::builder_localnet(Epoch(1)) .call_function(Default::default(), "foo", args![]) .with_inputs(inputs) .build_and_seal(&k); diff --git a/crates/engine/examples/native_points_calibrate.rs b/crates/engine/examples/native_points_calibrate.rs index 7680f9fea8..daaa606aa4 100644 --- a/crates/engine/examples/native_points_calibrate.rs +++ b/crates/engine/examples/native_points_calibrate.rs @@ -58,7 +58,7 @@ use tari_crypto::{ }; use tari_engine::fees::FeeTable; use tari_engine_types::{fees::FeeSource, stealth::validate_transfer}; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::stealth::StealthTransferStatement; use tari_template_test_tooling::{ TemplateTest, @@ -302,7 +302,7 @@ fn measure(host: Host) -> Calibration { // One measured execution: returns (WasmExecution points, wall-clock sample in ms). let run = |test: &mut TemplateTest, rounds: u64| -> (u64, Sample) { let execute = |test: &mut TemplateTest| -> (u64, f64) { - let tx = Transaction::builder_localnet() + let tx = Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, MAX_FEE) .call_function(bench, "bench_div_u64", args![rounds]) .build_and_seal(&key); diff --git a/crates/engine/tests/access_rules.rs b/crates/engine/tests/access_rules.rs index 964afcb053..281b3b2c6c 100644 --- a/crates/engine/tests/access_rules.rs +++ b/crates/engine/tests/access_rules.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeMap, HashMap}; use tari_engine::runtime::{ActionIdent, LockError, RuntimeError}; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::{ args::ComponentAction, types::{ @@ -60,7 +60,7 @@ mod component_access_rules { .default(AccessRule::DenyAll); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_configured_rules", args![ // Owner OwnerRule::ByAccessRule(owner_rule), @@ -86,7 +86,7 @@ mod component_access_rules { .clone(); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "set_value", args![1]) .build_and_seal(&owner2_key), vec![owner2_proof], @@ -95,7 +95,7 @@ mod component_access_rules { let (unauth_proof, _, unauth_key) = test.create_owner_proof(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "set_value", args![1]) .build_and_seal(&unauth_key), vec![unauth_proof], @@ -118,7 +118,7 @@ mod component_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_configured_rules", args![ // Owner OwnerRule::OwnedBySigner, @@ -139,7 +139,7 @@ mod component_access_rules { // Access Denied let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "set_value", args![1]) .build_and_seal(&user_key), vec![user_proof.clone()], @@ -152,7 +152,7 @@ mod component_access_rules { // Allow user to call set_value test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "set_component_access_rules", args![ ComponentAccessRules::new() .add_method_rule("set_value", rule!(non_fungible(user_proof.clone()))) @@ -163,14 +163,14 @@ mod component_access_rules { ); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "set_value", args![1]) .build_and_seal(&user_key), vec![user_proof.clone()], ); test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "set_component_access_rules", args![ ComponentAccessRules::new().default(AccessRule::AllowAll) ]) @@ -189,7 +189,7 @@ mod component_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_configured_rules", args![ // Owner OwnerRule::None, @@ -210,7 +210,7 @@ mod component_access_rules { // Owner cannot set access rules let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "set_component_access_rules", args![ ComponentAccessRules::new().default(AccessRule::AllowAll) ]) @@ -241,7 +241,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_configured_rules", args![ // Owner OwnerRule::OwnedBySigner, @@ -262,7 +262,7 @@ mod resource_access_rules { // User cannot get tokens let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_tokens", args![10]) .put_last_instruction_output_on_workspace("tokens") .call_method(owner_account, "deposit", args![Workspace("tokens")]) @@ -274,7 +274,7 @@ mod resource_access_rules { // Owner can get tokens test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_tokens", args![10]) .put_last_instruction_output_on_workspace("tokens") .call_method(owner_account, "deposit", args![Workspace("tokens")]) @@ -284,7 +284,7 @@ mod resource_access_rules { // Owner gives user permission to withdraw tokens test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "update_tokens_access_rule", args![ ResourceAuthAction::Withdraw, rule!(non_fungible(user_proof.clone())) @@ -295,7 +295,7 @@ mod resource_access_rules { // User can get tokens, and deposit them in the owners account (deposit is default allow) test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_tokens", args![10]) .put_last_instruction_output_on_workspace("tokens") .call_method(owner_account, "deposit", args![Workspace("tokens")]) @@ -316,7 +316,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_configured_rules", args![ // Owner - Everyone! OwnerRule::ByAccessRule(AccessRule::AllowAll), @@ -337,7 +337,7 @@ mod resource_access_rules { // Give the user a withdraw and deposit badge test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_badge_by_name", args!["withdraw"]) .put_last_instruction_output_on_workspace("withdraw_perm") .call_method(component_address, "take_badge_by_name", args!["deposit"]) @@ -363,7 +363,7 @@ mod resource_access_rules { // Now try recall them. This won't succeed because recall only respects access rules not ownership, so the call // is denied for the owner. let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "recall_badge", args![ user_badge_vault_id, "withdraw" @@ -387,7 +387,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "using_badge_rules", args![]) .build_and_seal(&owner_key), vec![owner_proof.clone()], @@ -413,7 +413,7 @@ mod resource_access_rules { // User cannot get the tokens let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_tokens", args![10]) .put_last_instruction_output_on_workspace("tokens") .call_method(user_account, "deposit", args![Workspace("tokens")]) @@ -425,7 +425,7 @@ mod resource_access_rules { // Give the user a withdraw and deposit badge test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_badge_by_name", args!["withdraw"]) .put_last_instruction_output_on_workspace("withdraw_perm") .call_method(component_address, "take_badge_by_name", args!["deposit"]) @@ -438,7 +438,7 @@ mod resource_access_rules { // User can take tokens let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(user_account, "create_proof_by_non_fungible_ids", args![ badge_resource, vec![ @@ -467,7 +467,7 @@ mod resource_access_rules { // Recall badge test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "recall_badge", args![ user_badge_vault_id, "withdraw" @@ -478,7 +478,7 @@ mod resource_access_rules { // User can no longer withdraw tokens let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(user_account, "create_proof_for_resource", args![badge_resource]) .put_last_instruction_output_on_workspace("proof") .call_method(user_account, "withdraw", args![token_resource, 10]) @@ -503,7 +503,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "using_resource_rules", args![]) .build_and_seal(&owner_key), vec![owner_proof.clone()], @@ -527,7 +527,7 @@ mod resource_access_rules { // User cannot get the tokens let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(access_rules_component, "take_tokens", args![10]) .put_last_instruction_output_on_workspace("tokens") .call_method(user_account, "deposit", args![Workspace("tokens")]) @@ -539,7 +539,7 @@ mod resource_access_rules { // Give the user a badge test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(access_rules_component, "mint_new_badge", args![]) .put_last_instruction_output_on_workspace("permission") .call_method(user_account, "deposit", args![Workspace("permission")]) @@ -549,7 +549,7 @@ mod resource_access_rules { // User can take tokens test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(user_account, "create_proof_by_amount", args![badge_resource, 1]) .put_last_instruction_output_on_workspace("proof") .call_method(access_rules_component, "take_tokens_using_proof", args![ @@ -574,7 +574,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_configured_rules", args![ // Owner OwnerRule::OwnedBySigner, @@ -607,7 +607,7 @@ mod resource_access_rules { // Take some tokens, generate a proof from the bucket (locking them up), and then try withdrawing them let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_tokens", args![1000]) .put_last_instruction_output_on_workspace("tokens") .call_method(owner_account, "deposit", args![Workspace("tokens")]) @@ -624,7 +624,7 @@ mod resource_access_rules { // Drop the proof before withdraw/deposit test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_tokens", args![1000]) .put_last_instruction_output_on_workspace("tokens") .call_method(owner_account, "deposit", args![Workspace("tokens")]) @@ -652,7 +652,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "using_resource_rules", args![]) .build_and_seal(&owner_key), vec![owner_proof.clone()], @@ -678,7 +678,7 @@ mod resource_access_rules { // Try to take tokens without proof. Even though I'm the owner of the resource, the scope does not carry over // when cross-template calls are made. let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(cross_call_template, "call_component_with_args", args![ component_address, "take_tokens", @@ -695,7 +695,7 @@ mod resource_access_rules { // Do a cross template call using a proof test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "mint_new_badge", args![]) .put_last_instruction_output_on_workspace("badge") .call_method(owner_account, "deposit", args![Workspace("badge")]) @@ -727,7 +727,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "using_badge_rules", args![]) .build_and_seal(&owner_key), vec![owner_proof.clone()], @@ -751,7 +751,7 @@ mod resource_access_rules { // User cannot get the tokens let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_tokens", args![10]) .put_last_instruction_output_on_workspace("tokens") .call_method(user_account, "deposit", args![Workspace("tokens")]) @@ -763,7 +763,7 @@ mod resource_access_rules { // Give the user a withdraw and deposit badge test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_badge_by_name", args!["withdraw"]) .put_last_instruction_output_on_workspace("withdraw_perm") .call_method(component_address, "take_badge_by_name", args!["deposit"]) @@ -776,7 +776,7 @@ mod resource_access_rules { // Side case: we try deposit back the badges before we drop the proof. This is invalid. let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method( user_account, "withdraw_many_non_fungibles", @@ -818,7 +818,7 @@ mod resource_access_rules { // User can take tokens, using a proof obtained from a bucket test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method( user_account, "withdraw_many_non_fungibles", @@ -862,7 +862,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function( access_rules_template, "resource_actions_restricted_to_component", @@ -890,7 +890,7 @@ mod resource_access_rules { // Minting using a template function will fail let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "mint_resource", args![token_resource]) .put_last_instruction_output_on_workspace("tokens") .call_method(owner_account, "deposit", args![Workspace("tokens")]) @@ -902,7 +902,7 @@ mod resource_access_rules { // Minting in a component context will succeed test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "mint_more_tokens", args![1000]) .put_last_instruction_output_on_workspace("tokens") .call_method(owner_account, "deposit", args![Workspace("tokens")]) @@ -921,7 +921,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_auth_hook", args![true, "valid_auth_hook"]) .build_and_seal(&owner_key), vec![owner_proof.clone()], @@ -932,7 +932,7 @@ mod resource_access_rules { .unwrap(); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_tokens", args![10]) .put_last_instruction_output_on_workspace("tokens") .call_method(owner_account, "deposit", args![Workspace("tokens")]) @@ -950,7 +950,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_auth_hook", args![false, "valid_auth_hook"]) .build_and_seal(&owner_key), vec![owner_proof.clone()], @@ -961,7 +961,7 @@ mod resource_access_rules { .unwrap(); let result = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_tokens", args![10]) .put_last_instruction_output_on_workspace("tokens") .call_method(owner_account, "deposit", args![Workspace("tokens")]) @@ -985,7 +985,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_auth_hook", args![ true, "malicious_auth_hook_set_state" @@ -999,7 +999,7 @@ mod resource_access_rules { .unwrap(); let result = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_tokens", args![10]) .put_last_instruction_output_on_workspace("tokens") .call_method(user_account, "deposit", args![Workspace("tokens")]) @@ -1023,7 +1023,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_auth_hook", args![ true, "malicious_auth_hook_call_mut" @@ -1037,7 +1037,7 @@ mod resource_access_rules { .unwrap(); let result = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_tokens", args![10]) .put_last_instruction_output_on_workspace("tokens") .call_method(user_account, "deposit", args![Workspace("tokens")]) @@ -1063,7 +1063,7 @@ mod resource_access_rules { // User has a state component let state_template = test.get_template_address("State"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(state_template, "restricted", args![]) .build_and_seal(&user_key), vec![owner_proof.clone()], @@ -1076,7 +1076,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_auth_hook_attack_component", args![ state_component ]) @@ -1089,7 +1089,7 @@ mod resource_access_rules { .unwrap(); let result = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_tokens", args![10]) .put_last_instruction_output_on_workspace("tokens") .call_method(state_component, "set", args![1]) @@ -1124,7 +1124,7 @@ mod resource_access_rules { .iter() .for_each(|hook| { let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_auth_hook", args![true, hook]) .build_and_seal(test.secret_key()), vec![test.owner_proof()], @@ -1148,7 +1148,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_configured_rules", args![ OwnerRule::OwnedBySigner, ComponentAccessRules::new().default(AccessRule::AllowAll), @@ -1168,7 +1168,7 @@ mod resource_access_rules { new_metadata.insert("description", "updated"); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "set_tokens_metadata", args![new_metadata]) .build_and_seal(&user_key), vec![user_proof], @@ -1187,7 +1187,7 @@ mod resource_access_rules { let access_rules_template = test.get_template_address("AccessRulesTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_configured_rules", args![ OwnerRule::OwnedBySigner, ComponentAccessRules::new().default(AccessRule::AllowAll), @@ -1206,7 +1206,7 @@ mod resource_access_rules { new_metadata.insert("description", "updated by user"); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "set_tokens_metadata", args![new_metadata]) .build_and_seal(&user_key), vec![user_proof], @@ -1225,7 +1225,7 @@ mod resource_access_rules { // Withdraw starts denied; the updater is `OWNER`, so the owner can change the rule later. let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_configured_rules", args![ OwnerRule::OwnedBySigner, ComponentAccessRules::new().default(AccessRule::AllowAll), @@ -1242,7 +1242,7 @@ mod resource_access_rules { // Owner relaxes the withdraw rule. test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "update_tokens_access_rule", args![ ResourceAuthAction::Withdraw, rule!(non_fungible(user_proof.clone())) @@ -1253,7 +1253,7 @@ mod resource_access_rules { // User holding the new badge can now withdraw and deposit. test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_tokens", args![10]) .put_last_instruction_output_on_workspace("tokens") .call_method(owner_account, "deposit", args![Workspace("tokens")]) @@ -1273,7 +1273,7 @@ mod resource_access_rules { // Default ResourceAccessRules leaves the mint updater as `Locked`, so even the owner // cannot change the mint rule. test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_configured_rules", args![ OwnerRule::OwnedBySigner, ComponentAccessRules::new().default(AccessRule::AllowAll), @@ -1292,7 +1292,7 @@ mod resource_access_rules { .unwrap(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "update_tokens_access_rule", args![ ResourceAuthAction::Mint, AccessRule::AllowAll @@ -1319,7 +1319,7 @@ mod resource_access_rules { // Withdraw rule starts denied; the updater requires the user's badge — not the owner. let updater_rule = rule!(non_fungible(user_proof.clone())); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(access_rules_template, "with_configured_rules", args![ OwnerRule::OwnedBySigner, ComponentAccessRules::new().default(AccessRule::AllowAll), @@ -1339,7 +1339,7 @@ mod resource_access_rules { // Owner cannot update — they do not hold the badge. let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "update_tokens_access_rule", args![ ResourceAuthAction::Withdraw, AccessRule::AllowAll @@ -1353,7 +1353,7 @@ mod resource_access_rules { // Badge holder (the "user" identity) can update. test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "update_tokens_access_rule", args![ ResourceAuthAction::Withdraw, AccessRule::AllowAll @@ -1364,7 +1364,7 @@ mod resource_access_rules { // And the relaxed rule is in effect. test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "take_tokens", args![10]) .put_last_instruction_output_on_workspace("tokens") .call_method(owner_account, "deposit", args![Workspace("tokens")]) diff --git a/crates/engine/tests/account.rs b/crates/engine/tests/account.rs index 9ad28405ba..2ce16c7f8c 100644 --- a/crates/engine/tests/account.rs +++ b/crates/engine/tests/account.rs @@ -4,7 +4,7 @@ use ootle_byte_type::ToByteType; use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; use tari_engine::runtime::{ActionIdent, RuntimeError}; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS; use tari_template_lib::types::{Amount, access_rules::ComponentAccessRules, constants::TARI_TOKEN, rule}; use tari_template_test_tooling::{ @@ -25,7 +25,7 @@ fn basic_faucet_transfer() { let result = template_test .build_and_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(sender_address, "withdraw", args![TARI_TOKEN, 100]) .put_last_instruction_output_on_workspace("foo_bucket") .call_method(receiver_address, "deposit", args![Workspace("foo_bucket")]) @@ -56,7 +56,7 @@ fn withdraw_from_account_prevented() { let (dest_address, non_owning_token, non_owning_key) = test.create_empty_account(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(source_account, "withdraw", args![TARI_TOKEN, 100]) .put_last_instruction_output_on_workspace("stolen_coins") .call_method(dest_address, "deposit", args![Workspace("stolen_coins")]) @@ -86,7 +86,7 @@ fn attempt_to_overwrite_account() { let null: Option<()> = None; let overwriting_tx = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) // Create component with the same ID // The create account instruction is idempotent, so we'll call the template directly to force an overwrite attempt .call_function( @@ -122,7 +122,7 @@ fn create_account_is_idempotent() { let source_account_pk = RistrettoPublicKey::from_secret_key(&source_account_sk); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) // Create component with the same ID .create_account(source_account_pk.to_byte_type()) // Signed by source account so that it can pay the fees for the new account creation @@ -156,7 +156,7 @@ fn create_account_is_idempotent_with_deposit() { let source_account_pk = RistrettoPublicKey::from_secret_key(&source_account_sk); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) // Create component with the same ID .create_account(source_account_pk.to_byte_type()) .put_last_instruction_output_on_workspace("account") @@ -194,7 +194,7 @@ fn gasless() { let fee_account_pk = RistrettoPublicKey::from_secret_key(&fee_account_sk); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(fee_account, 1000u64) .call_method(user_account, "withdraw", args![TARI_TOKEN, 100]) .put_last_instruction_output_on_workspace("b") @@ -227,7 +227,7 @@ fn custom_access_rules() { .default(rule!(allow_all)); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) // Create component with the same ID .create_account_custom::<&str>( public_key.to_byte_type(), @@ -251,7 +251,7 @@ fn custom_access_rules() { // We create another account and we we will withdraw from the custom one let (user2_account, user2_account_proof, user2_secret_key) = test.create_funded_account(); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(user_account, "withdraw", args![TARI_TOKEN, 100]) .put_last_instruction_output_on_workspace("b") .call_method(user2_account, "deposit", args![Workspace("b")]) @@ -268,7 +268,7 @@ fn take_from_bucket() { let (bob, _proof, _bob_sk) = test.create_empty_account(); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(alice, "withdraw_all", args![TARI_TOKEN]) .put_last_instruction_output_on_workspace("coins") .take_from_bucket("coins", 100u64, "foo_bucket") @@ -306,7 +306,7 @@ fn put_into_bucket_merges_same_resource() { let (bob, _proof, _bob_sk) = test.create_empty_account(); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(alice, "withdraw", args![TARI_TOKEN, 600u64]) .put_last_instruction_output_on_workspace("first") .call_method(alice, "withdraw", args![TARI_TOKEN, 400u64]) @@ -336,7 +336,7 @@ fn put_into_bucket_rejects_resource_mismatch() { // Mint a non-TARI fungible resource and fund alice with it. test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("faucet_address") .call_function(faucet_template, "mint_with_opts", args![ Amount::from(1_000_000u64), @@ -357,7 +357,7 @@ fn put_into_bucket_rejects_resource_mismatch() { .expect("TT resource not found"); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(alice, "withdraw", args![TARI_TOKEN, 100u64]) .put_last_instruction_output_on_workspace("tari_bucket") .call_method(alice, "withdraw", args![tt_resource, 100u64]) diff --git a/crates/engine/tests/address_allocation.rs b/crates/engine/tests/address_allocation.rs index 0b835e8361..a9ddf85de1 100644 --- a/crates/engine/tests/address_allocation.rs +++ b/crates/engine/tests/address_allocation.rs @@ -3,7 +3,7 @@ use tari_engine::runtime::TransactionCommitError; use tari_engine_types::{component::derive_component_address_from_public_key, indexed_value::IndexedValue}; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS; use tari_template_lib::types::{ComponentAddress, ResourceAddress}; use tari_template_test_tooling::{TemplateTest, support::assert_error::assert_reject_reason, xtr_faucet_component}; @@ -15,7 +15,7 @@ fn it_allocates_addresses_in_template_code() { let mut test = TemplateTest::new(CRATE_PATH, ["tests/templates/address_allocation"]); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(test.get_template_address("AddressAllocationTest"), "create", args![]) .build_and_seal(test.secret_key()), vec![], @@ -67,7 +67,7 @@ fn it_fails_if_address_allocation_is_not_used() { let template_addr = test.get_template_address("AddressAllocationTest"); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "drop_component_allocation", args![]) .build_and_seal(test.secret_key()), vec![], @@ -75,7 +75,7 @@ fn it_fails_if_address_allocation_is_not_used() { assert_reject_reason(reason, TransactionCommitError::DanglingAddressAllocations { count: 1 }); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "drop_resource_allocation", args![]) .build_and_seal(test.secret_key()), vec![], @@ -89,7 +89,7 @@ fn it_fails_if_instruction_allocated_addresses_are_not_used() { let mut test = TemplateTest::new(CRATE_PATH, ["tests/templates/address_allocation"]); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("my_addr") .allocate_resource_address("my_res") .build_and_seal(test.secret_key()), @@ -106,7 +106,7 @@ fn it_allocates_an_address_using_instructions() { let template_addr = test.get_template_address("AddressAllocationTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("my_addr") .allocate_resource_address("my_res") .call_function(template_addr, "get_component_allocation_address", args![Workspace( @@ -155,7 +155,7 @@ fn it_allows_calls_to_component_using_the_allocated_address() { let template_addr = test.get_template_address("AddressAllocationTest"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("my_addr") .allocate_resource_address("my_res") .call_function(template_addr, "create_from_allocations", args![ @@ -185,7 +185,7 @@ fn it_allows_calls_to_component_using_a_component_on_the_workspace() { derive_component_address_from_public_key(&ACCOUNT_TEMPLATE_ADDRESS, &test.to_public_key_bytes()); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .create_account(test.to_public_key_bytes()) // Put the created account address on the workspace, and .put_last_instruction_output_on_workspace("account") diff --git a/crates/engine/tests/airdrop.rs b/crates/engine/tests/airdrop.rs index 3003ede895..e935202b68 100644 --- a/crates/engine/tests/airdrop.rs +++ b/crates/engine/tests/airdrop.rs @@ -4,7 +4,7 @@ use ootle_byte_type::ToByteType; use tari_engine_types::substate::SubstateId; use tari_ootle_common_types::substate_type::SubstateType; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{Amount, ComponentAddress}; use tari_template_test_tooling::TemplateTest; @@ -24,7 +24,7 @@ fn airdrop() { let total_supply: Amount = test.call_method(airdrop, "total_supply", args![], vec![test.owner_proof()]); assert_eq!(total_supply, Amount::from(100u64)); - let builder = Transaction::builder_localnet().then(|builder| { + let builder = Transaction::builder_localnet(Epoch(1)).then(|builder| { // Create 50 accounts (0..50).fold(builder, |builder, _| { let (_, owner_public_key, _) = test.create_owner_proof(); @@ -45,7 +45,7 @@ fn airdrop() { test.call_method::<()>(airdrop, "open_airdrop", args![], vec![test.owner_proof()]); test.build_and_execute( - Transaction::builder_localnet().then(|builder| { + Transaction::builder_localnet(Epoch(1)).then(|builder| { addresses.iter().fold(builder, |builder, addr| { builder.call_method(airdrop, "add_recipient", args![addr]) }) @@ -55,7 +55,7 @@ fn airdrop() { .unwrap_success(); let result = test.build_and_execute( - Transaction::builder_localnet().then(|builder| { + Transaction::builder_localnet(Epoch(1)).then(|builder| { addresses.iter().fold(builder, |builder, addr| { builder .call_method(airdrop, "claim_any", args![addr]) diff --git a/crates/engine/tests/asserts.rs b/crates/engine/tests/asserts.rs index 3998196ef1..ef67b34fa8 100644 --- a/crates/engine/tests/asserts.rs +++ b/crates/engine/tests/asserts.rs @@ -5,7 +5,7 @@ use std::vec; use tari_crypto::ristretto::RistrettoSecretKey; use tari_engine::runtime::{AssertError, RuntimeError}; -use tari_ootle_transaction::{Assertion, CheckOrd, Instruction, Transaction, args, args::WorkspaceOffsetId}; +use tari_ootle_transaction::{Assertion, CheckOrd, Epoch, Instruction, Transaction, args, args::WorkspaceOffsetId}; use tari_template_lib::types::{ ComponentAddress, NonFungibleAddress, @@ -52,7 +52,7 @@ mod assert_bucket_contains { let mut test: AssertTest = setup(); test.template_test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(test.account, "withdraw", args![test.faucet_resource, 1000u64]) .put_last_instruction_output_on_workspace("faucet_bucket") .assert_bucket_contains_at_least("faucet_bucket", test.faucet_resource, FAUCET_WITHDRAWAL_AMOUNT) @@ -72,7 +72,7 @@ mod assert_bucket_contains { let invalid_resource_address = NFT_FAUCET_RESOURCE_ADDRESS; let reason = test.template_test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(test.account, "withdraw", args![test.faucet_resource, 1000u64]) .put_last_instruction_output_on_workspace("faucet_bucket") .assert_bucket_contains_at_least("faucet_bucket", invalid_resource_address, FAUCET_WITHDRAWAL_AMOUNT) @@ -98,7 +98,7 @@ mod assert_bucket_contains { let min_amount = FAUCET_WITHDRAWAL_AMOUNT + 1; let reason = test.template_test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(test.account, "withdraw", args![test.faucet_resource, 1000u64]) .put_last_instruction_output_on_workspace("faucet_bucket") // Passes @@ -127,7 +127,7 @@ mod assert_bucket_contains { let mut test: AssertTest = setup(); let reason = test.template_test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(test.account, "withdraw", args![test.faucet_resource, 1000u64]) // we are going to assert a workspace value that is NOT a bucket .call_method(test.account, "get_balances", args![]) @@ -151,7 +151,7 @@ mod assert_bucket_contains { let mut test: AssertTest = setup(); let reason = test.template_test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(test.account, "withdraw", args![test.faucet_resource, 1000u64]) .put_last_instruction_output_on_workspace("faucet_bucket") // we are going to assert a key that does not exist in the workspace @@ -184,7 +184,7 @@ mod assert_is_not_null { let mut test: AssertTest = setup(); let reason = test.template_test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) // we are going to assert a key that does not exist in the workspace // assert_bucket_contains would panic if called with a non-existing key .add_instruction(Instruction::Assert { @@ -207,7 +207,7 @@ mod assert_is_not_null { let mut test: AssertTest = setup(); let reason = test.template_test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(test.account, "withdraw", args![test.faucet_resource, 1000u64]) .put_last_instruction_output_on_workspace("faucet_bucket") .assert_workspace_item_is_not_null("faucet_bucket") @@ -232,7 +232,7 @@ mod assert_bucket_contains_non_fungibles { let mut test: AssertTest = setup(); test.template_test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(NFT_FAUCET_COMPONENT_ADDRESS, "mint", args![5, tari_bor::Value::Null]) .put_last_instruction_output_on_workspace("faucet_bucket") .assert_bucket_contains_non_fungibles_all("faucet_bucket", NFT_FAUCET_RESOURCE_ADDRESS, vec![ @@ -270,7 +270,7 @@ mod assert_bucket_contains_non_fungibles { // Take some tokens from the faucet to get a bucket with non-fungibles let reason = test.template_test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(test.account, "withdraw", args![test.faucet_resource, 1000u64]) .put_last_instruction_output_on_workspace("faucet_bucket") .assert_bucket_contains_non_fungibles_all("faucet_bucket", test.faucet_resource, vec![]) @@ -290,7 +290,7 @@ mod assert_bucket_contains_non_fungibles { let mut test: AssertTest = setup(); let reason = test.template_test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(NFT_FAUCET_COMPONENT_ADDRESS, "mint", args![5, tari_bor::Value::Null]) .put_last_instruction_output_on_workspace("faucet_bucket") .assert_bucket_contains_non_fungibles_all("faucet_bucket", NFT_FAUCET_RESOURCE_ADDRESS, vec![ @@ -313,7 +313,7 @@ mod assert_bucket_contains_non_fungibles { let mut test: AssertTest = setup(); let reason = test.template_test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(NFT_FAUCET_COMPONENT_ADDRESS, "mint", args![5, tari_bor::Value::Null]) .put_last_instruction_output_on_workspace("faucet_bucket") .assert_bucket_contains_non_fungibles_any("faucet_bucket", NFT_FAUCET_RESOURCE_ADDRESS, vec![ @@ -335,7 +335,7 @@ mod assert_bucket_contains_non_fungibles { let mut test: AssertTest = setup(); let reason = test.template_test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(NFT_FAUCET_COMPONENT_ADDRESS, "mint", args![5, tari_bor::Value::Null]) .put_last_instruction_output_on_workspace("faucet_bucket") .assert_bucket_contains_non_fungibles_none_of("faucet_bucket", NFT_FAUCET_RESOURCE_ADDRESS, vec![ @@ -358,7 +358,7 @@ mod assert_bucket_contains_non_fungibles { let mut test: AssertTest = setup(); let reason = test.template_test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(NFT_FAUCET_COMPONENT_ADDRESS, "mint", args![5, tari_bor::Value::Null]) .put_last_instruction_output_on_workspace("faucet_bucket") .assert_bucket_contains_non_fungibles_not_any_of("faucet_bucket", NFT_FAUCET_RESOURCE_ADDRESS, vec![ diff --git a/crates/engine/tests/burn.rs b/crates/engine/tests/burn.rs index b6791f6f46..b28a9f6649 100644 --- a/crates/engine/tests/burn.rs +++ b/crates/engine/tests/burn.rs @@ -1,7 +1,7 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{Amount, ComponentAddress}; use tari_template_test_tooling::{TemplateTest, support::confidential::generate_confidential_output_statement}; @@ -16,7 +16,7 @@ fn it_burns_all_resource_types() { initial_supply.output_revealed_amount = Amount::from(1000u64); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(recall_template, "new", args![initial_supply]) .build_and_seal(test.secret_key()), vec![], @@ -50,7 +50,7 @@ fn it_burns_all_resource_types() { } test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component, "burn_all", args![]) .build_and_seal(test.secret_key()), vec![], diff --git a/crates/engine/tests/complex_fee_payment.rs b/crates/engine/tests/complex_fee_payment.rs index 8a8de2fdef..04fe5b57b6 100644 --- a/crates/engine/tests/complex_fee_payment.rs +++ b/crates/engine/tests/complex_fee_payment.rs @@ -11,7 +11,7 @@ use tari_engine::fees::FeeTable; use tari_engine_types::{fees::FeeSource, limits::FREE_COMPUTE_GRACE_POINTS}; use tari_ootle_common_types::substate_type::SubstateType; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{Amount, ComponentAddress, ResourceAddress, constants::TARI_TOKEN}; use tari_template_test_tooling::TemplateTest; @@ -64,7 +64,7 @@ fn fee_paid_via_amm_swap_fits_within_grace() { // A non-TARI resource the account holds and will swap for TARI to pay its fee. let (token_faucet, token) = create_faucet(&mut test, "SWAP"); test.build_and_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(token_faucet, "take_free_coins_custom", args![Amount::from( 1_000_000_000u64 )]) @@ -77,7 +77,7 @@ fn fee_paid_via_amm_swap_fits_within_grace() { // A (TARI, token) pool, seeded with liquidity from the funded account. let pool = create_pool(&mut test, TARI_TOKEN, token); test.build_and_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(account, "withdraw", args![TARI_TOKEN, Amount::from(POOL_LIQUIDITY)]) .put_last_instruction_output_on_workspace("tari") .call_method(account, "withdraw", args![token, Amount::from(POOL_LIQUIDITY)]) @@ -99,7 +99,7 @@ fn fee_paid_via_amm_swap_fits_within_grace() { // The worst-case fee payment: source TARI by swapping `token` through the pool inside the fee // intent, then pay from the resulting bucket. Runs entirely on credit before `pay_fee`. - let tx = Transaction::builder_localnet() + let tx = Transaction::builder_localnet(Epoch(1)) .with_fee_instructions_builder(|builder| { builder .call_method(account, "withdraw", args![token, Amount::from(SWAP_INPUT)]) diff --git a/crates/engine/tests/compute_fee_budget.rs b/crates/engine/tests/compute_fee_budget.rs index 14e5812e29..62c74236da 100644 --- a/crates/engine/tests/compute_fee_budget.rs +++ b/crates/engine/tests/compute_fee_budget.rs @@ -11,7 +11,7 @@ use tari_crypto::ristretto::RistrettoSecretKey; use tari_engine::fees::FeeTable; use tari_engine_types::{commit_result::RejectReason, fees::FeeSource, limits::FREE_COMPUTE_GRACE_POINTS}; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{ComponentAddress, NonFungibleAddress, TemplateAddress}; use tari_template_test_tooling::TemplateTest; @@ -50,7 +50,7 @@ fn setup() -> Harness { let per_round = { let mut points = |rounds: u64| -> u64 { - let tx = Transaction::builder_localnet() + let tx = Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, 900_000_000u64) .call_function(bench, "bench_div_u64", args![rounds]) .build_and_seal(&key); @@ -101,7 +101,7 @@ fn underpaid_compute_is_capped_at_what_the_payment_funds() { } = setup(); let rounds = ABOVE_GRACE_POINTS / per_round; - let tx = Transaction::builder_localnet() + let tx = Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, FEE_PAYMENT) .call_function(bench, "bench_div_u64", args![rounds]) .build_and_seal(&key); @@ -153,7 +153,7 @@ fn grace_does_not_extend_past_the_fee_checkpoint() { } = setup(); let rounds = BELOW_GRACE_POINTS / per_round; - let tx = Transaction::builder_localnet() + let tx = Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, FEE_PAYMENT) .call_function(bench, "bench_div_u64", args![rounds]) .build_and_seal(&key); @@ -179,7 +179,7 @@ fn paying_more_raises_the_compute_allowance() { // Sized from the call's point cost (1 fee unit per point) plus margin for the fixed charges, // so it keeps funding the call as the grace constant moves. let fee_payment = ABOVE_GRACE_POINTS + 10_000_000; - let tx = Transaction::builder_localnet() + let tx = Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, fee_payment) .call_function(bench, "bench_div_u64", args![rounds]) .build_and_seal(&key); @@ -201,7 +201,7 @@ fn fee_intent_may_spend_compute_within_grace_before_paying() { } = setup(); let rounds = BELOW_GRACE_POINTS / per_round; - let tx = Transaction::builder_localnet() + let tx = Transaction::builder_localnet(Epoch(1)) .with_fee_instructions_builder(|builder| { builder .call_function(bench, "bench_div_u64", args![rounds]) @@ -227,7 +227,7 @@ fn fee_intent_cannot_exceed_grace_compute() { } = setup(); let rounds = ABOVE_GRACE_POINTS / per_round; - let tx = Transaction::builder_localnet() + let tx = Transaction::builder_localnet(Epoch(1)) .with_fee_instructions_builder(|builder| { builder .call_function(bench, "bench_div_u64", args![rounds]) diff --git a/crates/engine/tests/confidential.rs b/crates/engine/tests/confidential.rs index fc69667e04..3ee1cec15b 100644 --- a/crates/engine/tests/confidential.rs +++ b/crates/engine/tests/confidential.rs @@ -14,7 +14,7 @@ use tari_engine_types::{ substate::SubstateId, }; use tari_ootle_common_types::substate_type::SubstateType; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::{ models::Account, types::{ @@ -95,7 +95,7 @@ fn mint_more_later() { let withdraw_proof = generate_withdraw_proof(&mask, 100, None, 0u64); template_test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "take_free_coins", args![withdraw_proof.proof]) .put_last_instruction_output_on_workspace("coins") .call_method(user_account, "deposit", args![Workspace("coins")]) @@ -446,7 +446,7 @@ fn mint_and_transfer_revealed() { let withdraw = generate_withdraw_proof_with_inputs(&[], 123u64, 100, None, 23); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "take_free_coins", args![withdraw.proof]) .put_last_instruction_output_on_workspace("b") .call_method(user_account, "deposit", args![Workspace("b")]) @@ -466,7 +466,7 @@ fn mint_revealed_with_invalid_proof() { let (mut test, faucet, _faucet_resx) = setup(confidential_proof, None); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "mint_revealed_with_bad_range_proof", args![123]) .build_and_seal(test.secret_key()), vec![], @@ -510,7 +510,7 @@ fn mint_with_view_key() { let withdraw_proof = generate_withdraw_proof_with_view_key(&mask, 100, 55, Some(100 - 55), 0u64, view_key); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "take_free_coins", args![withdraw_proof.proof]) .put_last_instruction_output_on_workspace("coins") .call_method(user_account, "deposit", args![Workspace("coins")]) @@ -563,7 +563,7 @@ fn freeze_then_attempt_spend() { let owner = test.owner_proof(); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "freeze_confidential_outputs", args![vec![commitment]]) .build_and_seal(test.secret_key()), vec![owner.clone()], @@ -573,7 +573,7 @@ fn freeze_then_attempt_spend() { let withdraw_proof = generate_withdraw_proof(&mask, 100, None, 0u64); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "take_free_coins", args![withdraw_proof.proof.clone()]) .put_last_instruction_output_on_workspace("coins") .call_method(user_account, "deposit", args![Workspace("coins")]) @@ -586,14 +586,14 @@ fn freeze_then_attempt_spend() { }); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "unfreeze_confidential_outputs", args![vec![commitment]]) .build_and_seal(test.secret_key()), vec![owner], ); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "take_free_coins", args![withdraw_proof.proof]) .put_last_instruction_output_on_workspace("coins") .call_method(user_account, "deposit", args![Workspace("coins")]) @@ -616,7 +616,7 @@ fn unfreeze_and_spend_in_one_transaction_downs_the_output() { let owner = test.owner_proof(); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "freeze_confidential_outputs", args![vec![commitment]]) .build_and_seal(test.secret_key()), vec![owner.clone()], @@ -626,7 +626,7 @@ fn unfreeze_and_spend_in_one_transaction_downs_the_output() { let withdraw_proof = generate_withdraw_proof(&mask, 100, None, 0u64); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "unfreeze_confidential_outputs", args![vec![commitment]]) .call_method(faucet, "take_free_coins", args![withdraw_proof.proof]) .put_last_instruction_output_on_workspace("coins") @@ -659,7 +659,7 @@ fn minting_a_duplicate_commitment_is_rejected() { let (mint_proof, mint_mask, _change) = generate_confidential_output_statement(42, None); let owner = test.owner_proof(); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "mint_more", args![mint_proof.clone()]) .build_and_seal(test.secret_key()), vec![owner.clone()], @@ -668,7 +668,7 @@ fn minting_a_duplicate_commitment_is_rejected() { let commitment = commit_amount(&mint_mask, Amount::from(42u64)).unwrap().to_byte_type(); let address = ConfidentialOutputAddress::new(faucet_resx.as_resource_address().unwrap(), commitment); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "mint_more", args![mint_proof]) .build_and_seal(test.secret_key()), vec![owner], @@ -691,13 +691,13 @@ fn a_transaction_over_the_withdraw_cap_is_rejected() { // Revealed funds make each withdraw a revealed-only confidential withdraw: no commitments are needed // and the per-withdraw work is trivial, so only the per-transaction cap is exercised. test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "mint_revealed", args![Amount::new(max_withdraws as u128 + 1)]) .build_and_seal(test.secret_key()), vec![owner.clone()], ); - let mut builder = Transaction::builder_localnet(); + let mut builder = Transaction::builder_localnet(Epoch(1)); for _ in 0..=max_withdraws { builder = builder.call_method(faucet, "take_free_coins", args![ ConfidentialWithdrawProof::revealed_withdraw(Amount::new(1)) diff --git a/crates/engine/tests/cross_template.rs b/crates/engine/tests/cross_template.rs index 5e2a555e09..500f566ff0 100644 --- a/crates/engine/tests/cross_template.rs +++ b/crates/engine/tests/cross_template.rs @@ -7,7 +7,7 @@ use tari_engine_types::{ commit_result::{ExecuteResult, RejectReason}, limits, }; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{Amount, ComponentAddress, ResourceAddress, TemplateAddress}; use tari_template_test_tooling::{ TemplateTest, @@ -90,7 +90,7 @@ fn create_resource_and_fund_account(test: &mut TemplateTest, account: ComponentA let initial_supply = Amount::from(1_000_000_000_000u64); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("faucet_address") .call_function(faucet_template, "mint_with_opts", args![ initial_supply, @@ -134,7 +134,7 @@ fn it_allows_function_to_method_calls() { // create a new cross_template component, this time using a constructor that gets information from a method call let res = test.template_test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("component_1") .call_function(test.cross_call_template, "new_from_component", args![ Workspace("component_1"), @@ -227,7 +227,7 @@ fn it_fails_on_invalid_calls() { let result = test .template_test .try_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method( components.cross_template_component, "call_method_that_does_not_exist", @@ -262,7 +262,7 @@ fn it_does_not_propagate_permissions() { let result = test .template_test .try_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(components.cross_template_component, "malicious_withdraw", args![ victim_account, fungible_resource, @@ -336,7 +336,7 @@ fn it_fails_when_surpassing_recursion_limit_with_many_nested_components() { let result = test .template_test .try_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(last_composability_component, "get_nested_value", args![]) .build_and_seal(&private_key), vec![], @@ -357,7 +357,7 @@ fn it_fails_when_surpassing_recursion_limit() { let components = initialize_composability(&mut test); test.template_test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(components.cross_template_component, "recursion", args![ // -1 to account for the initial call max_call_depth - 1 @@ -366,7 +366,7 @@ fn it_fails_when_surpassing_recursion_limit() { vec![], ); let reason = test.template_test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(components.cross_template_component, "recursion", args![max_call_depth]) .build_and_seal(&private_key), vec![], diff --git a/crates/engine/tests/events.rs b/crates/engine/tests/events.rs index fab50fc506..cbd047849f 100644 --- a/crates/engine/tests/events.rs +++ b/crates/engine/tests/events.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_engine::runtime::RuntimeError; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_builtin::ACCOUNT_TEMPLATE_ADDRESS; use tari_template_lib::types::Amount; use tari_template_test_tooling::{ @@ -43,7 +43,7 @@ fn cannot_use_standard_topic() { let (_, _, private_key) = template_test.create_funded_account(); let invalid_topic = "std.mytopic"; let reason = template_test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(event_emitter_template, "test_function", args![invalid_topic]) .build_and_seal(&private_key), [].into(), @@ -65,7 +65,7 @@ fn builtin_vault_events() { // transfer some tokens between accounts let amount = Amount::from(100u64); let result = test.build_and_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(sender_address, "withdraw", args![TARI_TOKEN, amount]) .put_last_instruction_output_on_workspace("foo_bucket") .call_method(receiver_address, "deposit", args![Workspace("foo_bucket")]), diff --git a/crates/engine/tests/fees.rs b/crates/engine/tests/fees.rs index 2f55e2de39..388e94f4ba 100644 --- a/crates/engine/tests/fees.rs +++ b/crates/engine/tests/fees.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_engine_types::{commit_result::RejectReason, fees::FeeSource}; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{Amount, ComponentAddress, constants::STEALTH_TARI_RESOURCE_ADDRESS}; use tari_template_test_tooling::{TemplateTest, support::assert_error::assert_reject_reason, xtr_faucet_component}; @@ -19,7 +19,7 @@ fn deducts_fees_from_payments_and_refunds_the_rest() { test.enable_fees(); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, 1000u64) .call_function(test.get_template_address("State"), "new", args![]) .build_and_seal(&private_key), @@ -53,7 +53,7 @@ fn deducts_fees_when_transaction_fails() { test.enable_fees(); let result = test.execute_and_commit_on_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, 1000u64) .call_function(test.get_template_address("State"), "this_doesnt_exist", args![]) .build_and_seal(&private_key), @@ -81,7 +81,7 @@ fn deposit_from_faucet_then_pay() { test.enable_fees(); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_fee_instructions_builder(|builder| { builder // Faucet deposits free coins into the account @@ -120,7 +120,7 @@ fn another_account_pays_partially_for_fees() { const FAUCET_CAP: u64 = 100; let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) // Faucet pays a little .pay_fee_from_component(account_fee, Amount::from(FAUCET_CAP)) // Account pays the rest @@ -169,7 +169,7 @@ fn failed_fee_transaction() { test.enable_fees(); let result = test .try_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_fee_instructions_builder(|builder| { builder // This instruction will fail @@ -211,7 +211,7 @@ fn fail_partial_paid_fees() { const FEE_PAID: u64 = 100; let result = test.execute_expect_commit( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) // Pay less fees than the cost of the main transaction .pay_fee_from_component(account, Amount::from(FEE_PAID)) // These instructions should not be applied @@ -261,7 +261,7 @@ fn fail_pay_negative_fee() { test.enable_fees(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_fee_instructions_builder(|builder| builder.call_method(account, "pay_fee", args![-100])) .build_and_seal(&private_key), vec![owner_token], @@ -286,7 +286,7 @@ fn fail_pay_less_fees_than_fee_transaction() { let result = test .try_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_fee_instructions_builder(|builder| { (0u32..=0).fold(builder, |builder, i| { builder.call_method( @@ -366,7 +366,7 @@ fn fail_pay_too_little_no_fee_instruction() { test.enable_fees(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_fee_instructions_builder(|builder| { builder // These instructions should not be applied @@ -407,7 +407,7 @@ fn failure_pay_fee_in_main_instructions() { test.enable_fees(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) // Pay in fee intent, enough to pass this step .pay_fee_from_component(account, 100u64) // Call pay_fee in main instructions (outside fee instructions) not permitted @@ -431,7 +431,7 @@ fn dangling_bucket_pay_fees() { test.enable_fees(); let result = test.execute_and_commit_on_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, Amount::from(500u64)) .call_method(account, "withdraw", args![STEALTH_TARI_RESOURCE_ADDRESS, 10]) .put_last_instruction_output_on_workspace("dangling_bucket") @@ -471,7 +471,7 @@ fn template_load_fee_charged_once_per_template_per_transaction() { // Single State call — establishes the baseline TemplateLoad fee for {Account, State}. let single = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, 1000u64) .call_method(state, "set", args![1u32]) .build_and_seal(&private_key), @@ -481,7 +481,7 @@ fn template_load_fee_charged_once_per_template_per_transaction() { // Five State calls — same template touched five extra times. Without dedup, TemplateLoad // would scale with call count; with dedup it must match the single-call baseline. let many = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, 1000u64) .call_method(state, "set", args![1u32]) .call_method(state, "set", args![2u32]) diff --git a/crates/engine/tests/freeze.rs b/crates/engine/tests/freeze.rs index 7ef116dbe3..7828f57404 100644 --- a/crates/engine/tests/freeze.rs +++ b/crates/engine/tests/freeze.rs @@ -4,7 +4,7 @@ use std::collections::BTreeMap; use tari_engine::runtime::RuntimeError; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::{ args::VaultFreezeFlag, types::{ComponentAddress, ResourceAddress, VaultId}, @@ -21,7 +21,7 @@ fn it_freezes_vaults_containing_a_freezable_resource() { // Create a new Freeze component and deposit some resources into the account let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("freeze_comp") .call_function(template, "new", args![Workspace("freeze_comp")]) .call_method("freeze_comp", "withdraw", args![1000]) @@ -39,7 +39,7 @@ fn it_freezes_vaults_containing_a_freezable_resource() { // Freeze the account's vault test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component, "freeze", args![vault_id]) .build_and_seal(test.secret_key()), vec![test.owner_proof()], @@ -47,7 +47,7 @@ fn it_freezes_vaults_containing_a_freezable_resource() { // Attempt to withdraw from the frozen vault - FAIL let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(account, "withdraw", args![resource, 10]) .put_last_instruction_output_on_workspace("bucket") .call_method(account, "deposit", args![Workspace("bucket")]) @@ -62,7 +62,7 @@ fn it_freezes_vaults_containing_a_freezable_resource() { // Unfreeze the vault test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component, "unfreeze", args![vault_id]) .build_and_seal(test.secret_key()), vec![test.owner_proof()], @@ -70,7 +70,7 @@ fn it_freezes_vaults_containing_a_freezable_resource() { // Withdraw from the un-frozen vault - SUCCESS test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(account, "withdraw", args![resource, 10]) .put_last_instruction_output_on_workspace("bucket") .call_method(account, "deposit", args![Workspace("bucket")]) diff --git a/crates/engine/tests/fungible.rs b/crates/engine/tests/fungible.rs index e66ec8dce5..1a8978a345 100644 --- a/crates/engine/tests/fungible.rs +++ b/crates/engine/tests/fungible.rs @@ -3,7 +3,7 @@ use ootle_byte_type::ToByteType; use tari_engine_types::{crypto::commit_amount, vault::Vault}; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{Amount, ComponentAddress, ResourceType}; use tari_template_test_tooling::{ TemplateTest, @@ -18,7 +18,7 @@ fn it_does_not_overflow_when_minting_a_huge_initial_supply() { let template = test.get_template_address("Fungible"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template, "with_supply", args![Amount::MAX]) .build_and_seal(test.secret_key()), vec![], @@ -27,7 +27,7 @@ fn it_does_not_overflow_when_minting_a_huge_initial_supply() { let component: ComponentAddress = result.finalize.execution_results[0].decode().unwrap(); let all_to_confidential = generate_withdraw_proof_with_inputs(&[], u64::MAX, u64::MAX, None, 0); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component, "convert", args![all_to_confidential.proof]) .build_and_seal(test.secret_key()), vec![], @@ -53,7 +53,7 @@ fn it_does_not_overflow_when_minting_more_then_amount_max_fungible_tokens() { let template = test.get_template_address("Fungible"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template, "with_supply", args![i64::MAX]) .build_and_seal(test.secret_key()), vec![], @@ -62,7 +62,7 @@ fn it_does_not_overflow_when_minting_more_then_amount_max_fungible_tokens() { let component: ComponentAddress = result.finalize.execution_results[0].decode().unwrap(); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component, "fungible_mint_more", args![i64::MAX]) .call_method(component, "fungible_mint_more", args![i64::MAX]) .build_and_seal(test.secret_key()), @@ -83,12 +83,12 @@ fn it_does_not_overflow_when_minting_more_then_amount_max_confidential_tokens() let all_to_confidential = generate_withdraw_proof_with_inputs(&[], u64::MAX, u64::MAX, None, 0); test.execute_expect_success( - Transaction::builder_localnet().build_and_seal(test.secret_key()), + Transaction::builder_localnet(Epoch(1)).build_and_seal(test.secret_key()), vec![], ); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("fungible") .call_function(template, "with_address_and_supply", args![ Workspace("fungible"), @@ -105,7 +105,7 @@ fn it_does_not_overflow_when_minting_more_then_amount_max_confidential_tokens() let (more_supply2, _mask, _) = generate_confidential_output_statement(u64::MAX, None); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component, "confidential_mint_more", args![more_supply1]) .call_method(component, "confidential_mint_more", args![more_supply2]) .build_and_seal(test.secret_key()), diff --git a/crates/engine/tests/guessing_game.rs b/crates/engine/tests/guessing_game.rs index 1612b38912..1b96492874 100644 --- a/crates/engine/tests/guessing_game.rs +++ b/crates/engine/tests/guessing_game.rs @@ -1,7 +1,7 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::{ prelude::ComponentAddress, types::{NonFungibleAddress, NonFungibleId}, @@ -24,7 +24,7 @@ fn it_works() { // turned off by default. To turn them on use `test.enable_fees()`, you will then need to add fee instructions to // pay the fee. test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) // Allocate a new component address .allocate_component_address("guessing_game") // Construct the component by calling the `new` function of the template, passing the address allocation as an argument @@ -52,7 +52,7 @@ fn it_works() { // Just to demonstrate, we'll test an "unhappy path": user 1 makes a bad guess, since our template requires guesses // between 0 and 10. let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(game_address, "guess", args![100, user1_account]) .build_and_seal(&user1_secret), vec![], @@ -66,7 +66,7 @@ fn it_works() { // Let's make a correct guess with user 1 test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(game_address, "guess", args![5, user1_account]) .build_and_seal(&user1_secret), vec![], @@ -74,7 +74,7 @@ fn it_works() { // User 2 makes a correct guess test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(game_address, "guess", args![7, user2_account]) .build_and_seal(&user2_secret), vec![], @@ -82,7 +82,7 @@ fn it_works() { // User 3 makes a correct guess test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(game_address, "guess", args![3, user3_account]) .build_and_seal(&user3_secret), vec![], @@ -90,7 +90,7 @@ fn it_works() { // Now let's end the game and check the results. let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(game_address, "end_game_and_payout", args![]) .build_and_seal(test.secret_key()), vec![], diff --git a/crates/engine/tests/limits.rs b/crates/engine/tests/limits.rs index 8f5cb8bcc7..531e196e42 100644 --- a/crates/engine/tests/limits.rs +++ b/crates/engine/tests/limits.rs @@ -5,7 +5,7 @@ use std::slice; use tari_engine::{runtime::LimitError, wasm::WasmExecutionError}; use tari_engine_types::limits; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_abi::CallInfo; use tari_template_lib::types::bytes::Bytes; use tari_template_test_tooling::{TemplateTest, support::assert_error::assert_reject_reason}; @@ -24,7 +24,7 @@ fn max_call_size_limit() { let overhead = call_size - limits::ENGINE_LIMITS.max_call_size; test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function( template, "new", @@ -35,7 +35,7 @@ fn max_call_size_limit() { ); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function( template, "new", @@ -61,7 +61,7 @@ fn max_random_bytes_len_limit() { assert_eq!(bytes.len(), max_len as usize); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template, "request_random_bytes", args!(max_len + 1)) .build_and_seal(test.secret_key()), vec![], diff --git a/crates/engine/tests/metering_budget.rs b/crates/engine/tests/metering_budget.rs index 5a61820e4c..741f15c2dd 100644 --- a/crates/engine/tests/metering_budget.rs +++ b/crates/engine/tests/metering_budget.rs @@ -7,7 +7,7 @@ //! total is capped across calls. use tari_engine_types::{commit_result::RejectReason, fees::FeeSource}; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_test_tooling::TemplateTest; const CRATE_PATH: &str = env!("CARGO_MANIFEST_DIR"); @@ -27,7 +27,7 @@ fn per_transaction_budget_caps_total_across_calls() { test.enable_fees(); let call = |rounds: u64, n: usize| { - let mut builder = Transaction::builder_localnet().pay_fee_from_component(account, 900_000_000u64); + let mut builder = Transaction::builder_localnet(Epoch(1)).pay_fee_from_component(account, 900_000_000u64); for _ in 0..n { builder = builder.call_function(addr, "bench_div_u64", args![rounds]); } diff --git a/crates/engine/tests/native_compute_budget.rs b/crates/engine/tests/native_compute_budget.rs index eea7d82047..67e0c5d9ae 100644 --- a/crates/engine/tests/native_compute_budget.rs +++ b/crates/engine/tests/native_compute_budget.rs @@ -18,7 +18,7 @@ use tari_engine_types::{ limits::{FREE_COMPUTE_GRACE_POINTS, MAX_NATIVE_POINTS_PER_TRANSACTION, NativeExecutionPoints, STEALTH_LIMITS}, }; use tari_ootle_common_types::substate_type::SubstateType; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{ComponentAddress, NonFungibleAddress, ResourceAddress}; use tari_template_test_tooling::{ TemplateTest, @@ -90,7 +90,7 @@ fn setup_faucet( let template_addr = test.get_template_address(TEMPLATE_NAME); let initial_supply = transfer_data.statement.inputs_statement.revealed_amount; - let transaction = Transaction::builder_localnet() + let transaction = Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "new", args![ initial_supply, transfer_data.statement, @@ -136,7 +136,7 @@ fn unpaid_native_verification_traps_before_the_crypto_runs() { garbage.statement.outputs_statement.agg_range_proof = rp.try_into().unwrap(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_fee_instructions_builder(|builder| builder.stealth_transfer(faucet_resx, garbage.statement)) .build_and_seal(test.secret_key()), vec![], @@ -196,7 +196,7 @@ fn in_flight_wasm_counts_toward_the_native_allowance() { // Calibrate points-per-round with a paid transaction. let points_for = |test: &mut TemplateTest, rounds: u64| -> u64 { - let tx = Transaction::builder_localnet() + let tx = Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, 100_000_000u64) .call_method(faucet, "burn_compute", args![rounds]) .build_and_seal(&key); @@ -222,7 +222,7 @@ fn in_flight_wasm_counts_toward_the_native_allowance() { // Runs in the fee intent, which is where the credit applies — the main instructions are funded by // the payment alone and would trap on the WASM grind long before the native charge. let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_fee_instructions_builder(|builder| { builder.call_method(faucet, "burn_compute_then_transfer", args![rounds, transfer.statement]) }) @@ -260,7 +260,7 @@ fn paid_native_verification_is_charged() { let mask_badge = NonFungibleAddress::from_public_key(RistrettoPublicKey::from_secret_key(&mint.output_masks[0]).to_byte_type()); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, 900_000_000u64) .stealth_transfer(faucet_resx, transfer.statement) .finish() @@ -316,7 +316,7 @@ fn view_key_surcharge_is_charged_per_output() { let mask_badge = NonFungibleAddress::from_public_key(RistrettoPublicKey::from_secret_key(&mint.output_masks[0]).to_byte_type()); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, 900_000_000u64) .stealth_transfer(faucet_resx, transfer.statement) .finish() diff --git a/crates/engine/tests/no_concurrency.rs b/crates/engine/tests/no_concurrency.rs index e4282f99ce..d44bef434a 100644 --- a/crates/engine/tests/no_concurrency.rs +++ b/crates/engine/tests/no_concurrency.rs @@ -1,7 +1,7 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_test_tooling::{TemplateTest, support::assert_error::assert_reject_reason}; const TEMPLATE_NAME: &str = "NoConcurrency"; @@ -13,7 +13,7 @@ fn it_panics_if_template_attempts_to_spawn_a_thread() { let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function( test.get_template_address(TEMPLATE_NAME), "try_to_spawn_a_thread_static", diff --git a/crates/engine/tests/no_std.rs b/crates/engine/tests/no_std.rs index caaa308b2c..d467fa8b4f 100644 --- a/crates/engine/tests/no_std.rs +++ b/crates/engine/tests/no_std.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_engine_types::indexed_value::IndexedValue; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::ComponentAddress; use tari_template_test_tooling::{TemplateTest, support::assert_error::assert_reject_reason}; @@ -15,7 +15,7 @@ fn it_can_call_a_method() { let template = test.get_template_address("NoStdCounter"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("no_std_counter") .call_function(template, "with_address", args![Workspace("no_std_counter")]) .call_method("no_std_counter", "increment", args![]) @@ -44,7 +44,7 @@ fn it_can_call_a_function() { let template = test.get_template_address("NoStdCounter"); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template, "simple", args![]) .build_and_seal(test.secret_key()), vec![], @@ -58,7 +58,7 @@ fn it_panics() { let template = test.get_template_address("NoStdCounter"); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template, "panic_works", args![]) .build_and_seal(test.secret_key()), vec![], diff --git a/crates/engine/tests/publish_template.rs b/crates/engine/tests/publish_template.rs index fc4d442866..5078ad1c0c 100644 --- a/crates/engine/tests/publish_template.rs +++ b/crates/engine/tests/publish_template.rs @@ -13,7 +13,7 @@ use tari_engine_types::{ published_template::{PublishedTemplateAddress, TemplateBlob}, substate::{SubstateId, SubstateValue}, }; -use tari_ootle_transaction::Transaction; +use tari_ootle_transaction::{Epoch, Transaction}; use tari_template_test_tooling::{ TemplateTest, compile::compile_template, @@ -32,7 +32,7 @@ fn publish_template_success() { PublishedTemplateAddress::from_author_and_binary_hash(&public_key.to_byte_type(), &expected_binary_hash); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account_address, 200_000u64) .publish_template(template.into_code()) .build_and_seal(&account_key), @@ -63,7 +63,7 @@ fn publish_template_invalid_binary() { let mut test = TemplateTest::new(CRATE_PATH, &[] as &[&str]); let (account_address, owner_proof, account_key, _) = test.create_funded_account_with_keypair(); let result = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account_address, 200_000u64) // Main intent instruction #1 .publish_template(vec![1u8, 2, 3]) @@ -85,7 +85,7 @@ fn publish_template_too_big_binary() { let random_wasm_binary = generate_random_binary(limits::ENGINE_LIMITS.max_template_binary_size_bytes + 1); let wasm_binary_size = random_wasm_binary.len(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account_address, 200_000u64) // SAFETY: We are intentionally publishing an oversized binary to test size limits. .publish_template(unsafe { TemplateBlob::new_unchecked(random_wasm_binary) }) @@ -106,7 +106,7 @@ fn rejects_more_than_one_publish_template() { // The per-transaction publish-template cap is checked before any binary is validated, so these (invalid) binaries // never reach WASM validation: the transaction is rejected for carrying too many PublishTemplate instructions. - let mut builder = Transaction::builder_localnet().pay_fee_from_component(account_address, 200_000u64); + let mut builder = Transaction::builder_localnet(Epoch(1)).pay_fee_from_component(account_address, 200_000u64); for i in 0..=limits::MAX_PUBLISH_TEMPLATES_PER_TRANSACTION { builder = builder.publish_template(vec![i as u8]); } diff --git a/crates/engine/tests/recall.rs b/crates/engine/tests/recall.rs index 63c3d75646..9956e42add 100644 --- a/crates/engine/tests/recall.rs +++ b/crates/engine/tests/recall.rs @@ -4,7 +4,7 @@ use std::collections::BTreeMap; use ootle_byte_type::ToByteType; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{Amount, ComponentAddress, NonFungibleId, ResourceAddress, VaultId}; use tari_template_test_tooling::{ TemplateTest, @@ -23,7 +23,7 @@ fn it_recalls_all_resource_types() { initial_supply.output_revealed_amount = Amount::from(1000u64); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(recall_template, "new", args![initial_supply]) .build_and_seal(test.secret_key()), vec![], @@ -39,7 +39,7 @@ fn it_recalls_all_resource_types() { let withdraw = generate_withdraw_proof(&mask, 10, Some(980), 10u64); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(recall_component, "withdraw_some", args![withdraw.proof]) .put_last_instruction_output_on_workspace("buckets") .call_method(account, "deposit", args![Workspace("buckets.0")]) @@ -63,7 +63,7 @@ fn it_recalls_all_resource_types() { .to_byte_type(); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(recall_component, "recall_fungible", args![fungible_vault, 6]) .call_method(recall_component, "recall_non_fungibles", args![non_fungible_vault, [ NonFungibleId::from_u32(1) diff --git a/crates/engine/tests/reentrancy.rs b/crates/engine/tests/reentrancy.rs index 3c854ce625..f07d5ee583 100644 --- a/crates/engine/tests/reentrancy.rs +++ b/crates/engine/tests/reentrancy.rs @@ -3,7 +3,7 @@ use tari_engine::runtime::{LockError, LockState}; use tari_engine_types::lock::LockFlag; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::{ prelude::TARI_TOKEN, types::{ComponentAddress, constants::TARI}, @@ -19,7 +19,7 @@ fn it_prevents_reentrant_withdraw() { let (account, _, account_secret) = test.create_funded_account(); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(account, "withdraw", args![TARI_TOKEN, 1000 * TARI]) .put_last_instruction_output_on_workspace("bucket") .call_function(template_addr, "with_bucket", args![Workspace("bucket")]) @@ -32,7 +32,7 @@ fn it_prevents_reentrant_withdraw() { .unwrap(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(reentrancy, "reentrant_withdraw", args![1000]) .put_last_instruction_output_on_workspace("bucket") .call_method(account, "deposit", args![Workspace("bucket")]) @@ -52,7 +52,7 @@ fn it_allows_multiple_immutable_access_to_component() { let reentrancy: ComponentAddress = test.call_function("Reentrancy", "new", args![], vec![]); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(reentrancy, "reentrant_access_immutable", args![]) .build_and_seal(test.secret_key()), vec![], @@ -66,7 +66,7 @@ fn it_prevents_read_access_to_mutating_component() { let reentrancy: ComponentAddress = test.call_function("Reentrancy", "new", args![], vec![]); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(reentrancy, "reentrant_access", args![]) .build_and_seal(test.secret_key()), vec![], @@ -87,7 +87,7 @@ fn it_prevents_multiple_mutable_access_to_component() { let reentrancy: ComponentAddress = test.call_function("Reentrancy", "new", args![], vec![]); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(reentrancy, "reentrant_access_mut", args![]) .build_and_seal(test.secret_key()), vec![], diff --git a/crates/engine/tests/shenanigans.rs b/crates/engine/tests/shenanigans.rs index eb57358c94..9c0b6a64e1 100644 --- a/crates/engine/tests/shenanigans.rs +++ b/crates/engine/tests/shenanigans.rs @@ -7,7 +7,7 @@ use tari_engine_types::{ indexed_value::IndexedWellKnownTypes, resource_container::ResourceError, }; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::{ args::VaultAction, types::{Amount, ComponentAddress, ResourceType, constants::TARI_TOKEN}, @@ -23,7 +23,7 @@ fn it_rejects_dangling_vaults_in_constructor() { let template_addr = test.get_template_address(TEMPLATE_NAME); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "dangling_vault", args![]) .build_and_seal(test.secret_key()), vec![], @@ -42,7 +42,7 @@ fn it_rejects_dangling_vault_that_has_been_returned() { let template_addr = test.get_template_address(TEMPLATE_NAME); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "return_vault", args![]) .build_and_seal(test.secret_key()), vec![], @@ -58,7 +58,7 @@ fn it_rejects_dangling_vaults_in_component() { // Create with vault let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "with_vault", args![]) .build_and_seal(test.secret_key()), vec![], @@ -71,7 +71,7 @@ fn it_rejects_dangling_vaults_in_component() { let indexed = IndexedWellKnownTypes::from_value(component.state()).unwrap(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component_address, "drop_vault", args![]) .build_and_seal(test.secret_key()), vec![test.owner_proof()], @@ -88,7 +88,7 @@ fn it_rejects_dangling_resources() { let template_addr = test.get_template_address(TEMPLATE_NAME); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "dangling_resource", args![]) .build_and_seal(test.secret_key()), vec![], @@ -103,7 +103,7 @@ fn it_rejects_unknown_substate_ids() { let template_addr = test.get_template_address(TEMPLATE_NAME); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "non_existent_id", args![]) .build_and_seal(test.secret_key()), vec![], @@ -126,7 +126,7 @@ fn it_rejects_references_to_buckets_that_arent_in_scope() { let (account, owner_token, owner_key) = test.create_funded_account(); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "with_vault", args![]) .build_and_seal(&owner_key), vec![owner_token.clone()], @@ -137,7 +137,7 @@ fn it_rejects_references_to_buckets_that_arent_in_scope() { .unwrap(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(account, "withdraw", args![TARI_TOKEN, 1000]) .put_last_instruction_output_on_workspace("bucket") .call_method(shenanigans, "take_bucket_zero", args![]) @@ -155,7 +155,7 @@ fn it_rejects_double_ownership_of_vault() { let template_addr = test.get_template_address(TEMPLATE_NAME); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "with_vault_copy", args![]) .build_and_seal(test.secret_key()), vec![test.owner_proof()], @@ -177,7 +177,7 @@ fn it_prevents_access_to_vault_id_in_component_context() { }; let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "with_vault", args![]) .build_and_seal(test.secret_key()), vec![test.owner_proof()], @@ -188,7 +188,7 @@ fn it_prevents_access_to_vault_id_in_component_context() { .unwrap(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(shenanigans, "take_from_a_vault", args![vault_id, 1000]) .build_and_seal(test.secret_key()), vec![test.owner_proof()], @@ -208,7 +208,7 @@ fn it_prevents_access_to_out_of_scope_component() { let (account, _, _) = test.create_funded_account(); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "new", args![]) .build_and_seal(test.secret_key()), vec![test.owner_proof()], @@ -219,7 +219,7 @@ fn it_prevents_access_to_out_of_scope_component() { .unwrap(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(shenanigans, "empty_state_on_component", args![account]) .build_and_seal(test.secret_key()), vec![test.owner_proof()], @@ -246,7 +246,7 @@ fn it_disallows_calls_on_vaults_that_are_not_owned_by_current_component() { }; let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function( template_addr, "attempt_to_steal_funds_using_cross_template_call", @@ -276,7 +276,7 @@ fn it_disallows_vault_access_if_vault_is_not_owned() { }; let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "ref_stolen_vault", args![vault_id]) .build_and_seal(test.secret_key()), vec![test.owner_proof()], @@ -294,7 +294,7 @@ fn it_disallows_minting_different_resource_type() { let (account, _, _) = test.create_empty_account(); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "new", args![]) .build_and_seal(test.secret_key()), vec![], @@ -305,7 +305,7 @@ fn it_disallows_minting_different_resource_type() { .unwrap(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component, "mint_different_resource_type", args![]) .put_last_instruction_output_on_workspace("bucket") .call_method(account, "deposit", args![Workspace("bucket")]) @@ -335,7 +335,7 @@ fn it_does_not_bring_non_owned_vault_id_into_scope() { }; let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "with_stolen_vault", args![vault_id]) .put_last_instruction_output_on_workspace("bucket") .call_method(account, "deposit", args![Workspace("bucket")]) @@ -364,7 +364,7 @@ fn it_disallows_withdraws_from_vaults_outside_of_component_context() { )]); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "take_from_hardcoded_vault", args![]) .put_last_instruction_output_on_workspace("bucket") .call_method(account, "deposit", args![Workspace("bucket")]) @@ -378,7 +378,7 @@ fn it_disallows_withdraws_from_vaults_outside_of_component_context() { }); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "take_from_vault_and_return_bucket", args![vault_id,]) .put_last_instruction_output_on_workspace("bucket") .call_method(account, "deposit", args![Workspace("bucket")]) @@ -410,7 +410,7 @@ fn it_disallows_withdraws_from_vaults_outside_of_owning_component() { )]); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "new", args![]) .build_and_seal(test.secret_key()), vec![], @@ -421,7 +421,7 @@ fn it_disallows_withdraws_from_vaults_outside_of_owning_component() { .unwrap(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component, "take_from_hardcoded_vault_in_component_context", args![]) .put_last_instruction_output_on_workspace("bucket") .call_method(account, "deposit", args![Workspace("bucket")]) diff --git a/crates/engine/tests/signature.rs b/crates/engine/tests/signature.rs index 956dc04b3b..23f6ac7d92 100644 --- a/crates/engine/tests/signature.rs +++ b/crates/engine/tests/signature.rs @@ -11,7 +11,7 @@ use tari_ootle_common_types::{ crypto::create_key_pair_from_seed, substate_type::SubstateType, }; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{ Amount, ComponentAddress, @@ -51,7 +51,7 @@ fn setup(allow_list: Vec) -> (TemplateTest, ComponentAddress) { let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS); let template_addr = test.get_template_address(TEMPLATE_NAME); - let transaction = Transaction::builder_localnet() + let transaction = Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "new", args![allow_list]) .build_and_seal(test.secret_key()); @@ -76,7 +76,7 @@ fn claim_with_valid_signature() { let transfer = stealth::generate_transfer_data(NO_INPUTS, 1_000_000_000_000u64, Some(1_000_000_000_000), 0); let signature = sign_it(&s1); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "claim_funds", args![p1, signature, transfer.statement]) .build_and_seal(test.secret_key()), vec![test.owner_proof()], @@ -106,7 +106,7 @@ fn multi_claim() { let sig1 = sign_it(&s1); let sig2 = sign_it(&s2); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "claim_funds", args![p1, sig1, transfer1.statement]) .call_method(faucet, "claim_funds", args![p2, sig2, transfer2.statement]) .build_and_seal(test.secret_key()), @@ -123,7 +123,7 @@ fn bad_signature() { let transfer = stealth::generate_transfer_data(NO_INPUTS, 1000u64, Some(1000), 0); let sig1 = sign_it_with(&s1, b"A different message"); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "claim_funds", args![p1, sig1, transfer.statement]) .build_and_seal(test.secret_key()), vec![test.owner_proof()], @@ -144,7 +144,7 @@ fn check_signature_api() { let good_sig = sign_it(&s1); let bad_sig = sign_it_with(&s1, b"A different message"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "check_sig", args![p1, bad_sig]) .call_function(template_addr, "check_sig", args![p1, good_sig]) // Bad public key diff --git a/crates/engine/tests/spend_script.rs b/crates/engine/tests/spend_script.rs index 1074f4d18e..bf5926fbce 100644 --- a/crates/engine/tests/spend_script.rs +++ b/crates/engine/tests/spend_script.rs @@ -18,7 +18,7 @@ use tari_ootle_common_types::{ crypto::create_key_pair_from_seed, substate_type::SubstateType, }; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{ AccessRule, Amount, @@ -93,7 +93,7 @@ fn faucet_new_tx(test: &mut TemplateTest, mint: &StealthSecretTransferData) -> T test.enable_auto_add_proofs_from_signers(); let faucet_template = test.get_template_address(FAUCET_TEMPLATE); let initial_supply = mint.statement.inputs_statement.revealed_amount; - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(faucet_template, "new", args![ initial_supply, mint.statement.clone(), @@ -146,7 +146,7 @@ fn key_path_spend_authorised_by_signer_badge() { let transfer = spend_into(&mint, key_path(pk)); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -163,7 +163,7 @@ fn key_path_spend_rejected_without_signer_badge() { let transfer = spend_into(&mint, key_path(test.to_public_key_bytes())); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -185,7 +185,7 @@ fn script_path_access_rule_leaf_allows_spend() { let transfer = spend_into(&mint, key_path(test.to_public_key_bytes())); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -204,7 +204,7 @@ fn script_path_access_rule_leaf_denies_spend() { let transfer = spend_into(&mint, key_path(test.to_public_key_bytes())); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -248,7 +248,7 @@ fn multi_leaf_tree_spends_via_any_committed_leaf() { let transfer = stealth::generate_transfer_data([input], 0u64, [out(100, key_path(test.to_public_key_bytes()))], 0u64); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -287,7 +287,7 @@ fn revealing_a_leaf_not_in_the_tree_is_rejected() { let transfer = stealth::generate_transfer_data([input], 0u64, [out(100, key_path(test.to_public_key_bytes()))], 0u64); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -310,7 +310,7 @@ fn timelock_allows_spend_at_or_after_unlock_epoch() { // Output is key-path; the timelock is on the input being spent. let transfer = spend_into(&mint, key_path(test.to_public_key_bytes())); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -329,7 +329,7 @@ fn timelock_rejects_spend_before_unlock_epoch() { let transfer = spend_into(&mint, key_path(test.to_public_key_bytes())); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -351,7 +351,7 @@ fn covenant_allows_output_that_preserves_condition() { // The output carries the same covenant condition (so the same condition_root) -> the covenant is satisfied. let transfer = spend_into(&mint, covenant); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -369,7 +369,7 @@ fn covenant_rejects_output_that_changes_condition() { // The output changes the condition to a key path (different condition_root) -> the covenant rejects the spend. let transfer = spend_into(&mint, key_path(test.to_public_key_bytes())); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -389,7 +389,7 @@ fn covenant_rejects_output_with_added_key_path() { // would be key-spendable next block, escaping the covenant, so preserving the root alone must not satisfy it. let transfer = spend_into(&mint, key_and_conditions(test.to_public_key_bytes(), vec![covenant])); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -409,7 +409,7 @@ fn covenant_rejects_spend_with_no_stealth_outputs() { // preserves the condition, so the spend is rejected. let transfer = stealth::generate_transfer_data([mint.input_spec_for(0, 100)], 0u64, NO_OUTPUTS, 100u64); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -452,7 +452,7 @@ fn covenant_balance_allows_full_conservation() { // The full 100 units stay in the covenant -> conserved. let transfer = spend_with_covenant(&mint, &covenant, vec![out(100, covenant.clone())]); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -470,7 +470,7 @@ fn covenant_balance_rejects_value_leaving_partition() { // All 100 units go to a key-path output, leaving the covenant entirely -> rejected (allowance is zero). let transfer = spend_with_covenant(&mint, &covenant, vec![out(100, key_path(test.to_public_key_bytes()))]); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -494,7 +494,7 @@ fn covenant_balance_allows_withdrawal_within_allowance() { out(30, key_path(test.to_public_key_bytes())), ]); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -517,7 +517,7 @@ fn covenant_balance_rejects_withdrawal_over_allowance() { out(40, key_path(test.to_public_key_bytes())), ]); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -567,7 +567,7 @@ fn covenant_balance_verifies_each_partition_independently() { 0u64, ); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -594,7 +594,7 @@ fn covenant_balance_rejects_understated_withdrawal() { transfer.statement.covenant_claims[0].revealed_amount = Amount::from_u64(30); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -620,7 +620,7 @@ fn covenant_balance_allowance_vault_persists_across_spends() { let spend1 = spend_with_covenant(&mint, &vault, vec![out(70, vault.clone()), out(30, recipient.clone())]); let vault_70 = spend1.output_masks[0].clone(); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, spend1.statement) .finish() .seal(test.secret_key()), @@ -642,7 +642,7 @@ fn covenant_balance_allowance_vault_persists_across_spends() { ); let vault_40 = spend2.output_masks[0].clone(); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, spend2.statement) .finish() .seal(test.secret_key()), @@ -663,7 +663,7 @@ fn covenant_balance_allowance_vault_persists_across_spends() { 0u64, ); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, over.statement) .finish() .seal(test.secret_key()), @@ -682,7 +682,7 @@ fn always_reject_aborts_spend() { let transfer = spend_into(&mint, key_path(test.to_public_key_bytes())); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -701,7 +701,7 @@ fn read_only_sandbox_blocks_state_mutation() { let transfer = spend_into(&mint, key_path(test.to_public_key_bytes())); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -719,7 +719,7 @@ fn sandbox_denies_emit_event() { let transfer = spend_into(&mint, key_path(test.to_public_key_bytes())); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -743,7 +743,7 @@ fn sandbox_denies_cross_template_call() { let transfer = spend_into(&mint, key_path(test.to_public_key_bytes())); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -765,7 +765,7 @@ fn spend_script_exceeding_compute_budget_aborts() { // letting an expensive script stall execution. let transfer = spend_into(&mint, key_path(test.to_public_key_bytes())); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -809,7 +809,7 @@ fn signature_lock_allows_valid_signature() { let transfer = spend_into(&mint, key_path(test.to_public_key_bytes())); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -834,7 +834,7 @@ fn signature_lock_rejects_invalid_signature() { let transfer = spend_into(&mint, key_path(test.to_public_key_bytes())); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -858,7 +858,7 @@ fn assert_spend_rejected(function: &str, args: Vec, expected: &str) { let transfer = spend_into(&mint, key_path(test.to_public_key_bytes())); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -930,11 +930,11 @@ fn script_path_witness_increases_transaction_weight() { ); let key_spend = stealth::generate_transfer_data([MaskAndValue { mask, value: 100 }], 0u64, NO_OUTPUTS, 100u64); - let script_tx = Transaction::builder_localnet() + let script_tx = Transaction::builder_localnet(Epoch(1)) .stealth_transfer(STEALTH_TARI_RESOURCE_ADDRESS, script_spend.statement) .finish() .seal(test.secret_key()); - let key_tx = Transaction::builder_localnet() + let key_tx = Transaction::builder_localnet(Epoch(1)) .stealth_transfer(STEALTH_TARI_RESOURCE_ADDRESS, key_spend.statement) .finish() .seal(test.secret_key()); @@ -963,7 +963,7 @@ fn all(conditions: Vec) -> SpendCondition { /// Submits a stealth transfer spending the minted UTXO and asserts success. fn submit_expect_success(test: &mut TemplateTest, resx: ResourceAddress, transfer: StealthSecretTransferData) { test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -979,7 +979,7 @@ fn submit_expect_rejected( expected: &str, ) { let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(resx, transfer.statement) .finish() .seal(test.secret_key()), diff --git a/crates/engine/tests/stealth.rs b/crates/engine/tests/stealth.rs index 88c96541fc..cd823a151d 100644 --- a/crates/engine/tests/stealth.rs +++ b/crates/engine/tests/stealth.rs @@ -16,7 +16,7 @@ use tari_engine_types::{ resource_container::ResourceError, }; use tari_ootle_common_types::{crypto::create_key_pair_from_seed, substate_type::SubstateType}; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{ AccessRule, ComponentAddress, @@ -53,7 +53,7 @@ fn setup( let template_addr = test.get_template_address(TEMPLATE_NAME); let initial_supply = transfer_data.statement.inputs_statement.revealed_amount; - let transaction = Transaction::builder_localnet() + let transaction = Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "new", args![ initial_supply, transfer_data.statement, @@ -114,7 +114,7 @@ fn basic_transfer() { 0, ); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, transfer.statement) .finish() .add_signer(&test.to_public_key_bytes(), &mint.output_masks[0]) @@ -166,7 +166,7 @@ fn fee_intent_rejects_a_second_stealth_transfer() { let (first, second) = two_transfers(&mint); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_fee_instructions_builder(|builder| { builder .stealth_transfer(faucet_resx, first.statement) @@ -194,7 +194,7 @@ fn fee_intent_counts_a_stealth_transfer_performed_from_wasm() { let (first, second) = two_transfers(&mint); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_fee_instructions_builder(|builder| { builder.stealth_transfer(faucet_resx, first.statement).call_function( template_addr, @@ -221,7 +221,7 @@ fn main_intent_may_transfer_after_the_fee_intent_has() { let (first, second) = two_transfers(&mint); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_fee_instructions_builder(|builder| builder.stealth_transfer(faucet_resx, first.statement)) .stealth_transfer(faucet_resx, second.statement) .finish() @@ -254,7 +254,7 @@ fn programmatic_transfer() { 25, ); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "programmatic_transfer", args![transfer.statement]) .finish() .add_signer(&test.to_public_key_bytes(), &mint.output_masks[0]) @@ -291,7 +291,7 @@ fn transfer_with_revealed_outputs() { 700, ); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, transfer.statement) .put_last_instruction_output_on_workspace("bucket") .call_method(account, "deposit", args![Workspace("bucket")]) @@ -340,7 +340,7 @@ fn transfer_revealed_between_accounts() { ); let transfer_from_alice_to_bob = stealth::generate_transfer_data(NO_INPUTS, 100u64, [25, 25, 25], 25); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, transfer_from_faucet.statement) .put_last_instruction_output_on_workspace("withdrawn_funds_from_stealth_transfer") .call_method(alice, "deposit", args![Workspace( @@ -393,7 +393,7 @@ fn transfer_invalid_balance_in_statement() { 2, ); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, transfer_from_faucet.statement) .put_last_instruction_output_on_workspace("bucket") .call_method(alice, "deposit", args![Workspace("bucket")]) @@ -423,7 +423,7 @@ fn transfer_fails_if_transaction_is_not_signed_by_utxo_owner() { let transfer_from_faucet = stealth::generate_transfer_data([input], 0u64, [100], 0); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, transfer_from_faucet.statement) // Missing signer // .add_signer(&test.to_public_key_bytes(), &mint.output_masks[0]) @@ -466,7 +466,7 @@ fn transfer_invalid_range_proof_in_statement() { transfer_from_faucet.statement.outputs_statement.agg_range_proof = rp.try_into().unwrap(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, transfer_from_faucet.statement) .put_last_instruction_output_on_workspace("bucket") .call_method(alice, "deposit", args![Workspace("bucket")]) @@ -516,7 +516,7 @@ fn many_outputs_in_one_transfer() { eprintln!("Generated transfer in {:.2?}", timer.elapsed()); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, transfer_from_faucet.statement) .finish() .add_signer(&test.to_public_key_bytes(), &mint.output_masks[0]) @@ -570,7 +570,7 @@ fn mint_with_view_key() { &view_key, ); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, withdraw_proof.statement) .finish() .add_signer(&test.to_public_key_bytes(), &mint.output_masks[0]) @@ -630,7 +630,7 @@ fn freeze_then_attempt_spend() { .collect::>(); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "freeze_utxos", args![utxos]) .build_and_seal(test.secret_key()), vec![owner.clone()], @@ -638,7 +638,7 @@ fn freeze_then_attempt_spend() { // Try and spend a frozen output let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, transfer.statement.clone()) .finish() .add_signer(&test.to_public_key_bytes(), &mint.output_masks[0]) @@ -650,7 +650,7 @@ fn freeze_then_attempt_spend() { assert_reject_reason(reason, ResourceError::InvalidSpend { details: String::new() }); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "unfreeze_utxos", args![utxos]) .build_and_seal(test.secret_key()), vec![owner], @@ -658,7 +658,7 @@ fn freeze_then_attempt_spend() { // Should be able to spend now let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, transfer.statement) .finish() .add_signer(&test.to_public_key_bytes(), &mint.output_masks[0]) @@ -712,7 +712,7 @@ fn burn_then_attempt_spend() { .collect::>(); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet, "burn_utxos", args![utxos_and_proofs.clone()]) .build_and_seal(test.secret_key()), vec![owner.clone()], @@ -720,7 +720,7 @@ fn burn_then_attempt_spend() { // Try and spend a burnt outputs let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, transfer.statement.clone()) .build_and_seal(test.secret_key()), vec![], @@ -755,7 +755,7 @@ fn transfer_denied_by_resource_withdraw_rule() { let template_addr = test.get_template_address(TEMPLATE_NAME); let initial_supply = mint.statement.inputs_statement.revealed_amount; test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "new_withdraw_gated_by_signer", args![ initial_supply, mint.statement.clone() @@ -785,7 +785,7 @@ fn transfer_denied_by_resource_withdraw_rule() { // A non-issuer signer cannot authorise the transfer: the resource withdraw rule denies it. let (_attacker, _attacker_proof, attacker_sk) = test.create_empty_account(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, transfer.statement.clone()) .finish() .seal(&attacker_sk), @@ -795,7 +795,7 @@ fn transfer_denied_by_resource_withdraw_rule() { // The issuer (the withdraw authority) can: the same transfer now succeeds. let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, transfer.statement) .finish() .seal(test.secret_key()), @@ -856,7 +856,7 @@ fn transfer_restricted_by_access_rules_n_of_m() { // First try to spend with only 2 of the required 3 signatures let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, transfer.statement.clone()) .finish() .add_signer(&test_pk, &sk2) @@ -868,7 +868,7 @@ fn transfer_restricted_by_access_rules_n_of_m() { assert_access_denied_for_action(reason, ActionIdent::Native(NativeAction::StealthUtxoSpend)); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, transfer.statement) .finish() .add_signer(&test_pk, &sk2) @@ -914,7 +914,7 @@ fn transfer_restricted_by_access_rules_component_scope() { // Create the new outputs with the component-bound spend condition test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .stealth_transfer(faucet_resx, initial_transfer.statement.clone()) .finish() .seal(test.secret_key()), @@ -950,7 +950,7 @@ fn transfer_restricted_by_access_rules_component_scope() { // First try to spend in a template context let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function( test.get_template_address(TEMPLATE_NAME), "static_programmatic_transfer", @@ -965,7 +965,7 @@ fn transfer_restricted_by_access_rules_component_scope() { // Then, spend in the component context, which succeeds let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(component, "programmatic_transfer", args![transfer.statement]) .finish() .seal(test.secret_key()), diff --git a/crates/engine/tests/tariswap.rs b/crates/engine/tests/tariswap.rs index 0c5d69af0d..5a422f76e3 100644 --- a/crates/engine/tests/tariswap.rs +++ b/crates/engine/tests/tariswap.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_ootle_common_types::substate_type::SubstateType; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{Amount, ComponentAddress, NonFungibleAddress, ResourceAddress}; use tari_template_test_tooling::TemplateTest; @@ -101,7 +101,7 @@ fn fund_account( ) { template_test .build_and_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet_component, "take_free_coins", args![]) .put_last_instruction_output_on_workspace("free_coins") .call_method(account_address, "deposit", args![Workspace("free_coins")]), @@ -114,7 +114,7 @@ fn fund_account( fn swap(test: &mut TariSwapTest, input_resource: &ResourceAddress, output_resource: &ResourceAddress, amount: Amount) { test.template_test .build_and_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(test.account_address, "withdraw", args![input_resource, amount]) .put_last_instruction_output_on_workspace("input_bucket") .call_method(test.tariswap, "swap", args![Workspace("input_bucket"), output_resource]) @@ -129,7 +129,7 @@ fn swap(test: &mut TariSwapTest, input_resource: &ResourceAddress, output_resour fn add_liquidity(test: &mut TariSwapTest, a_amount: Amount, b_amount: Amount) { test.template_test .build_and_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(test.account_address, "withdraw", args![test.a_resource, a_amount]) .put_last_instruction_output_on_workspace("a_bucket") .call_method(test.account_address, "withdraw", args![test.b_resource, b_amount]) @@ -149,7 +149,7 @@ fn add_liquidity(test: &mut TariSwapTest, a_amount: Amount, b_amount: Amount) { fn remove_liquidity(test: &mut TariSwapTest, lp_amount: Amount) { test.template_test .build_and_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(test.account_address, "withdraw", args![test.lp_resource, lp_amount]) .put_last_instruction_output_on_workspace("lp_bucket") .call_method(test.tariswap, "remove_liquidity", args![Workspace("lp_bucket")]) diff --git a/crates/engine/tests/template_upgrade.rs b/crates/engine/tests/template_upgrade.rs index 55a704022d..3adc744b06 100644 --- a/crates/engine/tests/template_upgrade.rs +++ b/crates/engine/tests/template_upgrade.rs @@ -8,7 +8,7 @@ use tari_engine::{ }; use tari_engine_types::{commit_result::RejectReason, indexed_value::IndexedValue}; use tari_ootle_common_types::crypto::create_key_pair_from_seed; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::{ args::CallAction, types::{ComponentAddress, OwnerRule, VaultId}, @@ -36,7 +36,7 @@ fn create_component(test_mut: &mut TemplateTest) -> ComponentAddress { .collect::>(); test_mut.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(v1_template, "new", args![OwnerRule::OwnedBySigner, signers]) .finish() .seal(test_mut.secret_key()), @@ -69,7 +69,7 @@ fn it_migrates_to_a_new_template() { let v2_template = test.get_template_address("TemplateV2"); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .update_component_template_address_with_migrate(component, v2_template, "migrate_v1_to_v2", args![]) .call_method(component, "assert_correct", args![]) .finish() @@ -91,7 +91,7 @@ fn it_migrates_to_a_new_template_with_args() { let v2_template = test.get_template_address("TemplateV2"); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .update_component_template_address_with_migrate( component, v2_template, @@ -130,7 +130,7 @@ fn it_denies_migration_if_not_owner() { let (secret, _) = create_key_pair_from_seed(12); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .update_component_template_address_with_migrate( component, v2_template, @@ -162,7 +162,7 @@ fn it_fails_when_a_migration_drops_a_vault() { let v2_template = test.get_template_address("TemplateV2"); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .update_component_template_address_with_migrate( component, v2_template, @@ -196,7 +196,7 @@ fn it_fails_when_a_migration_panics() { let v2_template = test.get_template_address("TemplateV2"); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .update_component_template_address_with_migrate(component, v2_template, "faulty_migrate_panic", args![]) .finish() .seal(test.secret_key()), @@ -226,7 +226,7 @@ fn it_migrates_to_a_new_template_without_migration_call() { let v2_template = test.get_template_address("TemplateV2"); test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .update_component_template(component, v2_template) .call_method(component, "assert_correct", args![]) .finish() @@ -248,7 +248,7 @@ fn it_fails_when_a_migration_attempts_a_cross_template_call() { let v2_template = test.get_template_address("TemplateV2"); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .update_component_template_address_with_migrate( component, v2_template, @@ -276,7 +276,7 @@ fn it_disallows_calling_the_migration_function_directly() { let (secret, _) = create_key_pair_from_seed(12); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(v2_template, "migrate_v1_to_v2", args![]) .finish() .seal(&secret), diff --git a/crates/engine/tests/test.rs b/crates/engine/tests/test.rs index c6a39fd6de..87392e7f1f 100644 --- a/crates/engine/tests/test.rs +++ b/crates/engine/tests/test.rs @@ -32,7 +32,7 @@ use tari_engine_types::{ virtual_substate::{VirtualSubstate, VirtualSubstateId}, }; use tari_ootle_common_types::substate_type::SubstateType; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_builtin::{ACCOUNT_TEMPLATE_ADDRESS, NFT_FAUCET_TEMPLATE_ADDRESS, all_builtin_templates}; use tari_template_lib::{ models::NonFungible, @@ -293,7 +293,7 @@ fn test_engine_errors() { // check that public methods can still internally call private ones let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(test.get_template_address("Errors"), "invalid_engine_call", args![]) .build_and_seal(&Default::default()), vec![], @@ -381,7 +381,7 @@ fn test_random() { fn test_errors_on_infinite_loop() { let mut test = TemplateTest::new(CRATE_PATH, vec!["tests/templates/infinity_loop"]); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(test.get_template_address("InfinityLoopTest"), "infinity_loop", args![]) .build_and_seal(test.secret_key()), vec![], @@ -526,7 +526,7 @@ mod consensus { template_test.remove_virtual_substate(VirtualSubstateId::CurrentEpochHash); let reason = template_test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function( template_test.get_template_address("TestConsensus"), "current_epoch_hash", @@ -568,7 +568,7 @@ mod fungible { let owner_proof = test.owner_proof(); let result = test.build_and_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet_component, "burn_coins", args![500]) .call_method(faucet_component, "total_supply", args![]), vec![owner_proof.clone()], @@ -581,7 +581,7 @@ mod fungible { ); let result = test.build_and_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet_component, "burn_coins", args![ initial_supply - Amount::from(500u64) ]) @@ -596,7 +596,7 @@ mod fungible { ); test.build_and_execute( - Transaction::builder_localnet().call_method(faucet_component, "burn_coins", args![1]), + Transaction::builder_localnet(Epoch(1)).call_method(faucet_component, "burn_coins", args![1]), vec![], ) .expect_failure(); @@ -1020,7 +1020,7 @@ mod emoji_id { price: Amount, owner_proof: NonFungibleAddress, ) -> Result { - let transaction = Transaction::builder_localnet() + let transaction = Transaction::builder_localnet(Epoch(1)) .call_method(account_address, "withdraw", args![faucet_resource, price]) .put_last_instruction_output_on_workspace("payment") .call_method(emoji_id_minter, "mint", args![emoji_id, Workspace("payment")]) @@ -1049,7 +1049,7 @@ mod emoji_id { let price = Amount::from(20u64); let result = test .build_and_execute( - Transaction::builder_localnet().call_function(emoji_id_template, "new", args![ + Transaction::builder_localnet(Epoch(1)).call_function(emoji_id_template, "new", args![ TARI_TOKEN, max_emoji_id_len, price @@ -1148,7 +1148,7 @@ mod tickets { let price = Amount::from(20u64); let event_description = "My music festival".to_string(); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(ticket_template, "new", args![initial_supply, price, event_description]) .build_and_seal(&secret), vec![owner_proof.clone()], @@ -1168,7 +1168,7 @@ mod tickets { // buy a ticket test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(account_address, "withdraw", args![TARI_TOKEN, 20]) .put_last_instruction_output_on_workspace("payment") .call_method(ticket_seller, "buy_ticket", args![Workspace("payment")]) diff --git a/crates/engine/tests/transaction_receipt.rs b/crates/engine/tests/transaction_receipt.rs index a8ad31788d..2bc5cc1803 100644 --- a/crates/engine/tests/transaction_receipt.rs +++ b/crates/engine/tests/transaction_receipt.rs @@ -1,7 +1,7 @@ // Copyright 2026 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_ootle_transaction::{PrunedTransaction, Transaction, TransactionIntent, args}; +use tari_ootle_transaction::{Epoch, PrunedTransaction, Transaction, TransactionIntent, args}; use tari_template_test_tooling::TemplateTest; const CRATE_PATH: &str = env!("CARGO_MANIFEST_DIR"); @@ -13,7 +13,7 @@ const TEMPLATE_PATHS: [&str; 1] = ["tests/templates/state"]; fn committed_receipt_commits_to_the_transaction_intent() { let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS); - let transaction = Transaction::builder_localnet() + let transaction = Transaction::builder_localnet(Epoch(1)) .call_function(test.get_template_address("State"), "new", args![]) .build_and_seal(test.secret_key()); @@ -37,7 +37,7 @@ fn fee_intent_receipt_commits_to_the_transaction_intent() { let (account, owner_token, private_key) = test.create_funded_account(); test.enable_fees(); - let transaction = Transaction::builder_localnet() + let transaction = Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, 1000u64) .call_function(test.get_template_address("State"), "this_doesnt_exist", args![]) .build_and_seal(&private_key); @@ -56,10 +56,10 @@ fn receipt_does_not_match_a_different_intent() { let mut test = TemplateTest::new(CRATE_PATH, TEMPLATE_PATHS); let template = test.get_template_address("State"); - let transaction = Transaction::builder_localnet() + let transaction = Transaction::builder_localnet(Epoch(1)) .call_function(template, "new", args![]) .build_and_seal(test.secret_key()); - let other = Transaction::builder_localnet() + let other = Transaction::builder_localnet(Epoch(1)) .call_function(template, "new", args![]) .drop_all_proofs_in_workspace() .build_and_seal(test.secret_key()); diff --git a/crates/engine/tests/validator_fees.rs b/crates/engine/tests/validator_fees.rs index 92a73c8c91..e691670326 100644 --- a/crates/engine/tests/validator_fees.rs +++ b/crates/engine/tests/validator_fees.rs @@ -6,7 +6,7 @@ use tari_engine_types::{ ValidatorFeePool, substate::{Substate, SubstateId}, }; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::ValidatorFeePoolAddress; use tari_template_test_tooling::TemplateTest; @@ -31,7 +31,7 @@ fn test_claim_validator_fees_up_to() { // 1. Claim up to 60 TARI test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .claim_validator_fees_up_to(addr, 60u64) .put_last_instruction_output_on_workspace("bucket") .call_method(account, "deposit", args![Workspace("bucket")]) @@ -52,7 +52,7 @@ fn test_claim_validator_fees_up_to() { // 2. Claim all remaining (up to 1000) test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .claim_validator_fees_up_to(addr, 1000u64) .put_last_instruction_output_on_workspace("bucket") .call_method(account, "deposit", args![Workspace("bucket")]) diff --git a/crates/ootle_sdk_core/fixtures/account_balances/multi_vault_u64.json b/crates/ootle_sdk_core/fixtures/account_balances/multi_vault_u64.json index 87ce3d6fca..eb620eb20b 100644 --- a/crates/ootle_sdk_core/fixtures/account_balances/multi_vault_u64.json +++ b/crates/ootle_sdk_core/fixtures/account_balances/multi_vault_u64.json @@ -2,8 +2,8 @@ "name": "account_balances/multi_vault_u64", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "account_balances", diff --git a/crates/ootle_sdk_core/fixtures/address_codec/identity_esmeralda.json b/crates/ootle_sdk_core/fixtures/address_codec/identity_esmeralda.json index 6cc486d334..816870312b 100644 --- a/crates/ootle_sdk_core/fixtures/address_codec/identity_esmeralda.json +++ b/crates/ootle_sdk_core/fixtures/address_codec/identity_esmeralda.json @@ -2,8 +2,8 @@ "name": "address_codec/identity_esmeralda", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "format_identity_address", diff --git a/crates/ootle_sdk_core/fixtures/address_codec/identity_localnet_with_pay_ref.json b/crates/ootle_sdk_core/fixtures/address_codec/identity_localnet_with_pay_ref.json index 63895cc69c..a6020e9348 100644 --- a/crates/ootle_sdk_core/fixtures/address_codec/identity_localnet_with_pay_ref.json +++ b/crates/ootle_sdk_core/fixtures/address_codec/identity_localnet_with_pay_ref.json @@ -2,8 +2,8 @@ "name": "address_codec/identity_localnet_with_pay_ref", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "format_identity_address", diff --git a/crates/ootle_sdk_core/fixtures/address_codec/identity_mainnet.json b/crates/ootle_sdk_core/fixtures/address_codec/identity_mainnet.json index 15fa37176f..4775950a4f 100644 --- a/crates/ootle_sdk_core/fixtures/address_codec/identity_mainnet.json +++ b/crates/ootle_sdk_core/fixtures/address_codec/identity_mainnet.json @@ -2,8 +2,8 @@ "name": "address_codec/identity_mainnet", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "format_identity_address", diff --git a/crates/ootle_sdk_core/fixtures/address_codec/parse_component.json b/crates/ootle_sdk_core/fixtures/address_codec/parse_component.json index d4c8e4125a..c6abe41f2a 100644 --- a/crates/ootle_sdk_core/fixtures/address_codec/parse_component.json +++ b/crates/ootle_sdk_core/fixtures/address_codec/parse_component.json @@ -2,8 +2,8 @@ "name": "address_codec/parse_component", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "parse_address", diff --git a/crates/ootle_sdk_core/fixtures/address_codec/parse_identity_esmeralda.json b/crates/ootle_sdk_core/fixtures/address_codec/parse_identity_esmeralda.json index 11edab412c..c84a642cca 100644 --- a/crates/ootle_sdk_core/fixtures/address_codec/parse_identity_esmeralda.json +++ b/crates/ootle_sdk_core/fixtures/address_codec/parse_identity_esmeralda.json @@ -2,8 +2,8 @@ "name": "address_codec/parse_identity_esmeralda", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "parse_address", diff --git a/crates/ootle_sdk_core/fixtures/address_codec/parse_resource.json b/crates/ootle_sdk_core/fixtures/address_codec/parse_resource.json index 19f1a63ea7..9054b55381 100644 --- a/crates/ootle_sdk_core/fixtures/address_codec/parse_resource.json +++ b/crates/ootle_sdk_core/fixtures/address_codec/parse_resource.json @@ -2,8 +2,8 @@ "name": "address_codec/parse_resource", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "parse_address", diff --git a/crates/ootle_sdk_core/fixtures/address_derive/from_curve_pk.json b/crates/ootle_sdk_core/fixtures/address_derive/from_curve_pk.json index dd11a4a2f1..71bbc2f047 100644 --- a/crates/ootle_sdk_core/fixtures/address_derive/from_curve_pk.json +++ b/crates/ootle_sdk_core/fixtures/address_derive/from_curve_pk.json @@ -2,8 +2,8 @@ "name": "address_derive/from_curve_pk", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "derive_account_address", diff --git a/crates/ootle_sdk_core/fixtures/address_derive/from_recipient_pk.json b/crates/ootle_sdk_core/fixtures/address_derive/from_recipient_pk.json index e507556598..b54d07d366 100644 --- a/crates/ootle_sdk_core/fixtures/address_derive/from_recipient_pk.json +++ b/crates/ootle_sdk_core/fixtures/address_derive/from_recipient_pk.json @@ -2,8 +2,8 @@ "name": "address_derive/from_recipient_pk", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "derive_account_address", diff --git a/crates/ootle_sdk_core/fixtures/address_derive/from_seed_account_pk.json b/crates/ootle_sdk_core/fixtures/address_derive/from_seed_account_pk.json index 8eb4062bb0..39e46de9d9 100644 --- a/crates/ootle_sdk_core/fixtures/address_derive/from_seed_account_pk.json +++ b/crates/ootle_sdk_core/fixtures/address_derive/from_seed_account_pk.json @@ -2,8 +2,8 @@ "name": "address_derive/from_seed_account_pk", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "derive_account_address", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/address_component.json b/crates/ootle_sdk_core/fixtures/arg_dsl/address_component.json index 5ee75c9e6b..2dad590269 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/address_component.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/address_component.json @@ -2,8 +2,8 @@ "name": "arg_dsl/address_component", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/address_non_fungible.json b/crates/ootle_sdk_core/fixtures/arg_dsl/address_non_fungible.json index cc38a93225..9123febd40 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/address_non_fungible.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/address_non_fungible.json @@ -2,8 +2,8 @@ "name": "arg_dsl/address_non_fungible", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/address_resource.json b/crates/ootle_sdk_core/fixtures/arg_dsl/address_resource.json index d05c47164b..3f94bb41e1 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/address_resource.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/address_resource.json @@ -2,8 +2,8 @@ "name": "arg_dsl/address_resource", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/address_template.json b/crates/ootle_sdk_core/fixtures/arg_dsl/address_template.json index 5ce524de23..c19088aa25 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/address_template.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/address_template.json @@ -2,8 +2,8 @@ "name": "arg_dsl/address_template", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/address_tombstone.json b/crates/ootle_sdk_core/fixtures/arg_dsl/address_tombstone.json index dc3dbbbcba..5e699d734d 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/address_tombstone.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/address_tombstone.json @@ -2,8 +2,8 @@ "name": "arg_dsl/address_tombstone", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/address_transaction_receipt.json b/crates/ootle_sdk_core/fixtures/arg_dsl/address_transaction_receipt.json index 1443200195..de566baf29 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/address_transaction_receipt.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/address_transaction_receipt.json @@ -2,8 +2,8 @@ "name": "arg_dsl/address_transaction_receipt", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/address_utxo.json b/crates/ootle_sdk_core/fixtures/arg_dsl/address_utxo.json index ec1d499de7..d779d91642 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/address_utxo.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/address_utxo.json @@ -2,8 +2,8 @@ "name": "arg_dsl/address_utxo", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/address_validator_fee_pool.json b/crates/ootle_sdk_core/fixtures/arg_dsl/address_validator_fee_pool.json index bc1311907d..13dc2a7c28 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/address_validator_fee_pool.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/address_validator_fee_pool.json @@ -2,8 +2,8 @@ "name": "arg_dsl/address_validator_fee_pool", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/address_vault.json b/crates/ootle_sdk_core/fixtures/arg_dsl/address_vault.json index 73c4040903..09db0aebf6 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/address_vault.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/address_vault.json @@ -2,8 +2,8 @@ "name": "arg_dsl/address_vault", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/amount.json b/crates/ootle_sdk_core/fixtures/arg_dsl/amount.json index e950866bcc..a5fd64a005 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/amount.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/amount.json @@ -2,8 +2,8 @@ "name": "arg_dsl/amount", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/amount_above_2_pow_33.json b/crates/ootle_sdk_core/fixtures/arg_dsl/amount_above_2_pow_33.json index edd8237634..feeda83fbc 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/amount_above_2_pow_33.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/amount_above_2_pow_33.json @@ -2,8 +2,8 @@ "name": "arg_dsl/amount_above_2_pow_33", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/bool_false.json b/crates/ootle_sdk_core/fixtures/arg_dsl/bool_false.json index f4fac29607..612aae42b8 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/bool_false.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/bool_false.json @@ -2,8 +2,8 @@ "name": "arg_dsl/bool_false", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/bool_true.json b/crates/ootle_sdk_core/fixtures/arg_dsl/bool_true.json index dabeb9af0e..18ab558d39 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/bool_true.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/bool_true.json @@ -2,8 +2,8 @@ "name": "arg_dsl/bool_true", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/bytes.json b/crates/ootle_sdk_core/fixtures/arg_dsl/bytes.json index e7c62b4edc..0235a1fe0a 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/bytes.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/bytes.json @@ -2,8 +2,8 @@ "name": "arg_dsl/bytes", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/i64_min.json b/crates/ootle_sdk_core/fixtures/arg_dsl/i64_min.json index 2bb5184c37..ac5dd3950a 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/i64_min.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/i64_min.json @@ -2,8 +2,8 @@ "name": "arg_dsl/i64_min", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/i64_negative.json b/crates/ootle_sdk_core/fixtures/arg_dsl/i64_negative.json index 54d0e46b70..0c378dc9c3 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/i64_negative.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/i64_negative.json @@ -2,8 +2,8 @@ "name": "arg_dsl/i64_negative", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/i64_positive.json b/crates/ootle_sdk_core/fixtures/arg_dsl/i64_positive.json index 75c27246ec..40cc6bd1b0 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/i64_positive.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/i64_positive.json @@ -2,8 +2,8 @@ "name": "arg_dsl/i64_positive", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/list_empty.json b/crates/ootle_sdk_core/fixtures/arg_dsl/list_empty.json index e3ce9d01e0..0ff51c1089 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/list_empty.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/list_empty.json @@ -2,8 +2,8 @@ "name": "arg_dsl/list_empty", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/list_nested.json b/crates/ootle_sdk_core/fixtures/arg_dsl/list_nested.json index 63e280c1b8..1116c6b8a4 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/list_nested.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/list_nested.json @@ -2,8 +2,8 @@ "name": "arg_dsl/list_nested", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/list_of_addresses.json b/crates/ootle_sdk_core/fixtures/arg_dsl/list_of_addresses.json index 8da50132df..24e2bffa0c 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/list_of_addresses.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/list_of_addresses.json @@ -2,8 +2,8 @@ "name": "arg_dsl/list_of_addresses", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/list_of_nfids.json b/crates/ootle_sdk_core/fixtures/arg_dsl/list_of_nfids.json index 026d82af1b..e27adf02d6 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/list_of_nfids.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/list_of_nfids.json @@ -2,8 +2,8 @@ "name": "arg_dsl/list_of_nfids", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/metadata.json b/crates/ootle_sdk_core/fixtures/arg_dsl/metadata.json index f083de859d..b560dea401 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/metadata.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/metadata.json @@ -2,8 +2,8 @@ "name": "arg_dsl/metadata", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_str.json b/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_str.json index 1a8e126f4d..9522044b3f 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_str.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_str.json @@ -2,8 +2,8 @@ "name": "arg_dsl/nfid_str", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_u32.json b/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_u32.json index 708cf73efa..8817566061 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_u32.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_u32.json @@ -2,8 +2,8 @@ "name": "arg_dsl/nfid_u32", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_u64.json b/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_u64.json index b220e12298..2bba343641 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_u64.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_u64.json @@ -2,8 +2,8 @@ "name": "arg_dsl/nfid_u64", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_uuid.json b/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_uuid.json index c3d3719270..389504e1ef 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_uuid.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/nfid_uuid.json @@ -2,8 +2,8 @@ "name": "arg_dsl/nfid_uuid", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/optional_address.json b/crates/ootle_sdk_core/fixtures/arg_dsl/optional_address.json index 273d62900f..aa78dc4b29 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/optional_address.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/optional_address.json @@ -2,8 +2,8 @@ "name": "arg_dsl/optional_address", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/optional_none.json b/crates/ootle_sdk_core/fixtures/arg_dsl/optional_none.json index abe1f11c24..1b2c7963d2 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/optional_none.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/optional_none.json @@ -2,8 +2,8 @@ "name": "arg_dsl/optional_none", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/optional_some.json b/crates/ootle_sdk_core/fixtures/arg_dsl/optional_some.json index 45585ebbdd..781a830831 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/optional_some.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/optional_some.json @@ -2,8 +2,8 @@ "name": "arg_dsl/optional_some", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/string.json b/crates/ootle_sdk_core/fixtures/arg_dsl/string.json index 89b2676982..a4f96f866d 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/string.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/string.json @@ -2,8 +2,8 @@ "name": "arg_dsl/string", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/u64.json b/crates/ootle_sdk_core/fixtures/arg_dsl/u64.json index cd05e5be29..59cd2282de 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/u64.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/u64.json @@ -2,8 +2,8 @@ "name": "arg_dsl/u64", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/arg_dsl/u64_above_2_pow_33.json b/crates/ootle_sdk_core/fixtures/arg_dsl/u64_above_2_pow_33.json index 3ebc758fd6..0963ebf528 100644 --- a/crates/ootle_sdk_core/fixtures/arg_dsl/u64_above_2_pow_33.json +++ b/crates/ootle_sdk_core/fixtures/arg_dsl/u64_above_2_pow_33.json @@ -2,8 +2,8 @@ "name": "arg_dsl/u64_above_2_pow_33", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "encode_arg", diff --git a/crates/ootle_sdk_core/fixtures/cosign/seal_with_auth.json b/crates/ootle_sdk_core/fixtures/cosign/seal_with_auth.json index 55d405eb64..8a581ae2d5 100644 --- a/crates/ootle_sdk_core/fixtures/cosign/seal_with_auth.json +++ b/crates/ootle_sdk_core/fixtures/cosign/seal_with_auth.json @@ -2,8 +2,8 @@ "name": "cosign/seal_with_auth", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "cosign_seal_with_auth", diff --git a/crates/ootle_sdk_core/fixtures/generic_build/call_function.json b/crates/ootle_sdk_core/fixtures/generic_build/call_function.json index 18d5ea4925..d40766896d 100644 --- a/crates/ootle_sdk_core/fixtures/generic_build/call_function.json +++ b/crates/ootle_sdk_core/fixtures/generic_build/call_function.json @@ -2,8 +2,8 @@ "name": "generic_build/call_function", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_and_encode_instructions", @@ -38,7 +38,7 @@ ], "extra_inputs": [], "min_epoch": null, - "max_epoch": null, + "max_epoch": 1, "dry_run": false } }, @@ -86,7 +86,7 @@ } ], "is_seal_signer_authorized": true, - "max_epoch": null, + "max_epoch": 1, "min_epoch": null, "network": 38, "nonce": 0 diff --git a/crates/ootle_sdk_core/fixtures/generic_build/call_method_transfer.json b/crates/ootle_sdk_core/fixtures/generic_build/call_method_transfer.json index 03fc61453b..02f342dcac 100644 --- a/crates/ootle_sdk_core/fixtures/generic_build/call_method_transfer.json +++ b/crates/ootle_sdk_core/fixtures/generic_build/call_method_transfer.json @@ -2,8 +2,8 @@ "name": "generic_build/call_method_transfer", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_and_encode_instructions", diff --git a/crates/ootle_sdk_core/fixtures/generic_build/create_account.json b/crates/ootle_sdk_core/fixtures/generic_build/create_account.json index 57c88b87e6..77583e956e 100644 --- a/crates/ootle_sdk_core/fixtures/generic_build/create_account.json +++ b/crates/ootle_sdk_core/fixtures/generic_build/create_account.json @@ -2,8 +2,8 @@ "name": "generic_build/create_account", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_and_encode_instructions", @@ -38,7 +38,7 @@ ], "extra_inputs": [], "min_epoch": null, - "max_epoch": null, + "max_epoch": 1, "dry_run": false } }, @@ -87,7 +87,7 @@ } ], "is_seal_signer_authorized": true, - "max_epoch": null, + "max_epoch": 1, "min_epoch": null, "network": 38, "nonce": 0 diff --git a/crates/ootle_sdk_core/fixtures/generic_build/faucet_claim.json b/crates/ootle_sdk_core/fixtures/generic_build/faucet_claim.json index 2a3a5d7848..5e8b963f8e 100644 --- a/crates/ootle_sdk_core/fixtures/generic_build/faucet_claim.json +++ b/crates/ootle_sdk_core/fixtures/generic_build/faucet_claim.json @@ -2,8 +2,8 @@ "name": "generic_build/faucet_claim", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_and_encode_faucet_claim", @@ -64,7 +64,7 @@ "recipient_public_key": "fea009ee8681783f3e9ed6152d3b7fc204a7ba78cde9808cdedae3dd221af013", "fee": 2000, "min_epoch": null, - "max_epoch": null, + "max_epoch": 1, "dry_run": false } }, @@ -141,7 +141,7 @@ ], "instructions": [], "is_seal_signer_authorized": true, - "max_epoch": null, + "max_epoch": 1, "min_epoch": null, "network": 38, "nonce": 0 diff --git a/crates/ootle_sdk_core/fixtures/generic_build/publish_template.json b/crates/ootle_sdk_core/fixtures/generic_build/publish_template.json index bb866de2a9..2ed424ba25 100644 --- a/crates/ootle_sdk_core/fixtures/generic_build/publish_template.json +++ b/crates/ootle_sdk_core/fixtures/generic_build/publish_template.json @@ -2,8 +2,8 @@ "name": "generic_build/publish_template", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_and_encode_instructions", @@ -41,7 +41,7 @@ ], "extra_inputs": [], "min_epoch": null, - "max_epoch": null, + "max_epoch": 1, "dry_run": false } }, @@ -90,7 +90,7 @@ } ], "is_seal_signer_authorized": true, - "max_epoch": null, + "max_epoch": 1, "min_epoch": null, "network": 38, "nonce": 0 diff --git a/crates/ootle_sdk_core/fixtures/generic_build/self_funding_faucet.json b/crates/ootle_sdk_core/fixtures/generic_build/self_funding_faucet.json index c0d8f7d916..86e2df0289 100644 --- a/crates/ootle_sdk_core/fixtures/generic_build/self_funding_faucet.json +++ b/crates/ootle_sdk_core/fixtures/generic_build/self_funding_faucet.json @@ -2,8 +2,8 @@ "name": "generic_build/self_funding_faucet", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_and_encode_instructions", @@ -58,7 +58,7 @@ ], "extra_inputs": [], "min_epoch": null, - "max_epoch": null, + "max_epoch": 1, "dry_run": false } }, @@ -127,7 +127,7 @@ ], "instructions": [], "is_seal_signer_authorized": true, - "max_epoch": null, + "max_epoch": 1, "min_epoch": null, "network": 38, "nonce": 0 diff --git a/crates/ootle_sdk_core/fixtures/generic_build/workspace_pipe.json b/crates/ootle_sdk_core/fixtures/generic_build/workspace_pipe.json index e3e3e14b1a..1187f50090 100644 --- a/crates/ootle_sdk_core/fixtures/generic_build/workspace_pipe.json +++ b/crates/ootle_sdk_core/fixtures/generic_build/workspace_pipe.json @@ -2,8 +2,8 @@ "name": "generic_build/workspace_pipe", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_and_encode_instructions", @@ -65,7 +65,7 @@ ], "extra_inputs": [], "min_epoch": null, - "max_epoch": null, + "max_epoch": 1, "dry_run": false } }, @@ -143,7 +143,7 @@ } ], "is_seal_signer_authorized": true, - "max_epoch": null, + "max_epoch": 1, "min_epoch": null, "network": 38, "nonce": 0 diff --git a/crates/ootle_sdk_core/fixtures/keys/account_from_seed.json b/crates/ootle_sdk_core/fixtures/keys/account_from_seed.json index 884a8853df..f206cbf218 100644 --- a/crates/ootle_sdk_core/fixtures/keys/account_from_seed.json +++ b/crates/ootle_sdk_core/fixtures/keys/account_from_seed.json @@ -2,8 +2,8 @@ "name": "keys/account_from_seed", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "derive_account_key_from_seed", diff --git a/crates/ootle_sdk_core/fixtures/keys/view_from_seed.json b/crates/ootle_sdk_core/fixtures/keys/view_from_seed.json index 7eb2977564..f6d9718ed4 100644 --- a/crates/ootle_sdk_core/fixtures/keys/view_from_seed.json +++ b/crates/ootle_sdk_core/fixtures/keys/view_from_seed.json @@ -2,8 +2,8 @@ "name": "keys/view_from_seed", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "derive_view_key_from_seed", diff --git a/crates/ootle_sdk_core/fixtures/parse_finalized_result/accept.json b/crates/ootle_sdk_core/fixtures/parse_finalized_result/accept.json index 655b4d683d..1863af42c5 100644 --- a/crates/ootle_sdk_core/fixtures/parse_finalized_result/accept.json +++ b/crates/ootle_sdk_core/fixtures/parse_finalized_result/accept.json @@ -2,8 +2,8 @@ "name": "parse_finalized_result/accept", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "parse_finalized_result", diff --git a/crates/ootle_sdk_core/fixtures/parse_finalized_result/accept_fee_reject_rest.json b/crates/ootle_sdk_core/fixtures/parse_finalized_result/accept_fee_reject_rest.json index 0a8eb3e1a6..1b04b7ae7d 100644 --- a/crates/ootle_sdk_core/fixtures/parse_finalized_result/accept_fee_reject_rest.json +++ b/crates/ootle_sdk_core/fixtures/parse_finalized_result/accept_fee_reject_rest.json @@ -2,8 +2,8 @@ "name": "parse_finalized_result/accept_fee_reject_rest", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "parse_finalized_result", diff --git a/crates/ootle_sdk_core/fixtures/parse_finalized_result/dry_run.json b/crates/ootle_sdk_core/fixtures/parse_finalized_result/dry_run.json index 2cf0e7845d..f7dd1c4f9d 100644 --- a/crates/ootle_sdk_core/fixtures/parse_finalized_result/dry_run.json +++ b/crates/ootle_sdk_core/fixtures/parse_finalized_result/dry_run.json @@ -2,8 +2,8 @@ "name": "parse_finalized_result/dry_run", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "parse_finalized_result", diff --git a/crates/ootle_sdk_core/fixtures/parse_finalized_result/reject_epoch_expired.json b/crates/ootle_sdk_core/fixtures/parse_finalized_result/reject_epoch_expired.json index 29901a0ee0..6cc95087f6 100644 --- a/crates/ootle_sdk_core/fixtures/parse_finalized_result/reject_epoch_expired.json +++ b/crates/ootle_sdk_core/fixtures/parse_finalized_result/reject_epoch_expired.json @@ -2,8 +2,8 @@ "name": "parse_finalized_result/reject_epoch_expired", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "parse_finalized_result", diff --git a/crates/ootle_sdk_core/fixtures/public_transfer/large_amount.json b/crates/ootle_sdk_core/fixtures/public_transfer/large_amount.json index 52ebcc9575..d031a6af34 100644 --- a/crates/ootle_sdk_core/fixtures/public_transfer/large_amount.json +++ b/crates/ootle_sdk_core/fixtures/public_transfer/large_amount.json @@ -2,8 +2,8 @@ "name": "public_transfer/large_amount", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_and_encode_public_transfer", diff --git a/crates/ootle_sdk_core/fixtures/public_transfer/sample_single_key_basic.json b/crates/ootle_sdk_core/fixtures/public_transfer/sample_single_key_basic.json index 739051c22a..7246f8533e 100644 --- a/crates/ootle_sdk_core/fixtures/public_transfer/sample_single_key_basic.json +++ b/crates/ootle_sdk_core/fixtures/public_transfer/sample_single_key_basic.json @@ -2,8 +2,8 @@ "name": "sample/public_transfer_single_key_basic", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_and_encode_public_transfer", diff --git a/crates/ootle_sdk_core/fixtures/public_transfer/single_key_basic.json b/crates/ootle_sdk_core/fixtures/public_transfer/single_key_basic.json index 763ce738f5..db07031d1b 100644 --- a/crates/ootle_sdk_core/fixtures/public_transfer/single_key_basic.json +++ b/crates/ootle_sdk_core/fixtures/public_transfer/single_key_basic.json @@ -2,8 +2,8 @@ "name": "public_transfer/single_key_basic", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_and_encode_public_transfer", diff --git a/crates/ootle_sdk_core/fixtures/resolve_public_transfer/large_amount.json b/crates/ootle_sdk_core/fixtures/resolve_public_transfer/large_amount.json index 11a9cfd9f2..c8415a69ad 100644 --- a/crates/ootle_sdk_core/fixtures/resolve_public_transfer/large_amount.json +++ b/crates/ootle_sdk_core/fixtures/resolve_public_transfer/large_amount.json @@ -2,8 +2,8 @@ "name": "resolve_public_transfer/large_amount", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "resolve_and_encode_public_transfer", diff --git a/crates/ootle_sdk_core/fixtures/resolve_public_transfer/single_key_basic.json b/crates/ootle_sdk_core/fixtures/resolve_public_transfer/single_key_basic.json index 9fffdec2c2..e55c9d4db9 100644 --- a/crates/ootle_sdk_core/fixtures/resolve_public_transfer/single_key_basic.json +++ b/crates/ootle_sdk_core/fixtures/resolve_public_transfer/single_key_basic.json @@ -2,8 +2,8 @@ "name": "resolve_public_transfer/single_key_basic", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "resolve_and_encode_public_transfer", diff --git a/crates/ootle_sdk_core/fixtures/stealth_outputs_statement/single_output_no_view_key.json b/crates/ootle_sdk_core/fixtures/stealth_outputs_statement/single_output_no_view_key.json index 146c07bb92..08f0a0bd95 100644 --- a/crates/ootle_sdk_core/fixtures/stealth_outputs_statement/single_output_no_view_key.json +++ b/crates/ootle_sdk_core/fixtures/stealth_outputs_statement/single_output_no_view_key.json @@ -2,8 +2,8 @@ "name": "stealth_outputs_statement/single_output_no_view_key", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_stealth_outputs_statement", @@ -32,7 +32,7 @@ "revealed_input_amount": 0, "revealed_output_amount": 0, "min_epoch": null, - "max_epoch": null, + "max_epoch": 1, "dry_run": false, "pay_fee_from_revealed": false }, diff --git a/crates/ootle_sdk_core/fixtures/stealth_outputs_statement/single_output_with_view_key.json b/crates/ootle_sdk_core/fixtures/stealth_outputs_statement/single_output_with_view_key.json index dd0beb41ac..796cb97334 100644 --- a/crates/ootle_sdk_core/fixtures/stealth_outputs_statement/single_output_with_view_key.json +++ b/crates/ootle_sdk_core/fixtures/stealth_outputs_statement/single_output_with_view_key.json @@ -2,8 +2,8 @@ "name": "stealth_outputs_statement/single_output_with_view_key", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_stealth_outputs_statement", @@ -32,7 +32,7 @@ "revealed_input_amount": 0, "revealed_output_amount": 0, "min_epoch": null, - "max_epoch": null, + "max_epoch": 1, "dry_run": false, "pay_fee_from_revealed": false }, diff --git a/crates/ootle_sdk_core/fixtures/stealth_scan/decode_utxo.json b/crates/ootle_sdk_core/fixtures/stealth_scan/decode_utxo.json index f0692c73fa..a7b80449af 100644 --- a/crates/ootle_sdk_core/fixtures/stealth_scan/decode_utxo.json +++ b/crates/ootle_sdk_core/fixtures/stealth_scan/decode_utxo.json @@ -2,8 +2,8 @@ "name": "stealth_scan/decode_utxo", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "decode_stealth_utxo", diff --git a/crates/ootle_sdk_core/fixtures/stealth_scan/mine_basic.json b/crates/ootle_sdk_core/fixtures/stealth_scan/mine_basic.json index 3c3993cb9f..123d8987ca 100644 --- a/crates/ootle_sdk_core/fixtures/stealth_scan/mine_basic.json +++ b/crates/ootle_sdk_core/fixtures/stealth_scan/mine_basic.json @@ -2,8 +2,8 @@ "name": "stealth_scan/mine_basic", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "scan_stealth_output", diff --git a/crates/ootle_sdk_core/fixtures/stealth_scan/not_mine.json b/crates/ootle_sdk_core/fixtures/stealth_scan/not_mine.json index 6cff80b1d5..12d51cd369 100644 --- a/crates/ootle_sdk_core/fixtures/stealth_scan/not_mine.json +++ b/crates/ootle_sdk_core/fixtures/stealth_scan/not_mine.json @@ -2,8 +2,8 @@ "name": "stealth_scan/not_mine", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "scan_stealth_output", diff --git a/crates/ootle_sdk_core/fixtures/stealth_transfer/account_key_seal_with_revealed_input.json b/crates/ootle_sdk_core/fixtures/stealth_transfer/account_key_seal_with_revealed_input.json index e6ddacd93f..85d11e9fb8 100644 --- a/crates/ootle_sdk_core/fixtures/stealth_transfer/account_key_seal_with_revealed_input.json +++ b/crates/ootle_sdk_core/fixtures/stealth_transfer/account_key_seal_with_revealed_input.json @@ -2,8 +2,8 @@ "name": "stealth_transfer/account_key_seal_with_revealed_input", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_and_encode_stealth_transfer", @@ -32,7 +32,7 @@ "revealed_input_amount": 1000000, "revealed_output_amount": 0, "min_epoch": null, - "max_epoch": null, + "max_epoch": 1, "dry_run": false, "pay_fee_from_revealed": false }, @@ -132,7 +132,7 @@ } ], "is_seal_signer_authorized": true, - "max_epoch": null, + "max_epoch": 1, "min_epoch": null, "network": 38, "nonce": 0 diff --git a/crates/ootle_sdk_core/fixtures/stealth_transfer/revealed_output_multi.json b/crates/ootle_sdk_core/fixtures/stealth_transfer/revealed_output_multi.json index d144230ccc..183031064e 100644 --- a/crates/ootle_sdk_core/fixtures/stealth_transfer/revealed_output_multi.json +++ b/crates/ootle_sdk_core/fixtures/stealth_transfer/revealed_output_multi.json @@ -2,8 +2,8 @@ "name": "stealth_transfer/revealed_output_multi", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_and_encode_stealth_transfer", @@ -44,7 +44,7 @@ "revealed_input_amount": 2500000, "revealed_output_amount": 1500000, "min_epoch": null, - "max_epoch": null, + "max_epoch": 1, "dry_run": false, "pay_fee_from_revealed": false }, @@ -204,7 +204,7 @@ } ], "is_seal_signer_authorized": true, - "max_epoch": null, + "max_epoch": 1, "min_epoch": null, "network": 38, "nonce": 0 diff --git a/crates/ootle_sdk_core/fixtures/stealth_transfer/revealed_output_single.json b/crates/ootle_sdk_core/fixtures/stealth_transfer/revealed_output_single.json index a3c3500db0..5d09a60025 100644 --- a/crates/ootle_sdk_core/fixtures/stealth_transfer/revealed_output_single.json +++ b/crates/ootle_sdk_core/fixtures/stealth_transfer/revealed_output_single.json @@ -2,8 +2,8 @@ "name": "stealth_transfer/revealed_output_single", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_and_encode_stealth_transfer", @@ -32,7 +32,7 @@ "revealed_input_amount": 1500000, "revealed_output_amount": 500000, "min_epoch": null, - "max_epoch": null, + "max_epoch": 1, "dry_run": false, "pay_fee_from_revealed": false }, @@ -148,7 +148,7 @@ } ], "is_seal_signer_authorized": true, - "max_epoch": null, + "max_epoch": 1, "min_epoch": null, "network": 38, "nonce": 0 diff --git a/crates/ootle_sdk_core/fixtures/stealth_transfer/stealth_seal_with_input.json b/crates/ootle_sdk_core/fixtures/stealth_transfer/stealth_seal_with_input.json index 89fc0276f8..6f6739e716 100644 --- a/crates/ootle_sdk_core/fixtures/stealth_transfer/stealth_seal_with_input.json +++ b/crates/ootle_sdk_core/fixtures/stealth_transfer/stealth_seal_with_input.json @@ -2,8 +2,8 @@ "name": "stealth_transfer/stealth_seal_with_input", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "build_and_encode_stealth_transfer", @@ -60,7 +60,7 @@ "revealed_input_amount": 0, "revealed_output_amount": 0, "min_epoch": null, - "max_epoch": null, + "max_epoch": 1, "dry_run": false, "pay_fee_from_revealed": false }, @@ -144,7 +144,7 @@ } ], "is_seal_signer_authorized": true, - "max_epoch": null, + "max_epoch": 1, "min_epoch": null, "network": 38, "nonce": 0 diff --git a/crates/ootle_sdk_core/fixtures/substate_decode/component.json b/crates/ootle_sdk_core/fixtures/substate_decode/component.json index 856f695dd5..034752f88d 100644 --- a/crates/ootle_sdk_core/fixtures/substate_decode/component.json +++ b/crates/ootle_sdk_core/fixtures/substate_decode/component.json @@ -2,8 +2,8 @@ "name": "substate_decode/component", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "decode_substate", diff --git a/crates/ootle_sdk_core/fixtures/substate_decode/fungible_vault.json b/crates/ootle_sdk_core/fixtures/substate_decode/fungible_vault.json index f92cbbf376..4021213b22 100644 --- a/crates/ootle_sdk_core/fixtures/substate_decode/fungible_vault.json +++ b/crates/ootle_sdk_core/fixtures/substate_decode/fungible_vault.json @@ -2,8 +2,8 @@ "name": "substate_decode/fungible_vault", "schema_version": 1, "provenance": { - "core_version": "0.37.0", - "git_rev": "be2591689b664cddf465a7540277d41b09ecfd0d", + "core_version": "0.39.0", + "git_rev": "9f4833febee5c633a6fb2f0bd61dd1ce6d33c481", "generated_by": "ootle_sdk_core golden-vector generator" }, "operation": "decode_substate", diff --git a/crates/ootle_sdk_core/src/builder.rs b/crates/ootle_sdk_core/src/builder.rs index e8d56b64eb..a2c215530f 100644 --- a/crates/ootle_sdk_core/src/builder.rs +++ b/crates/ootle_sdk_core/src/builder.rs @@ -87,8 +87,12 @@ pub(crate) fn resolve_transfer_recipe(intent: &PublicTransferIntent) -> Result TransactionBuilder { - let mut builder = TransactionBuilder::new(network.as_byte()); +pub(crate) fn build_transfer_recipe_builder( + network: Network, + recipe: &TransferRecipe, + max_epoch: Epoch, +) -> TransactionBuilder { + let mut builder = TransactionBuilder::new(network.as_byte(), max_epoch); // Fee first. builder = builder.pay_fee_from_component(recipe.from_component, recipe.fee); @@ -108,7 +112,6 @@ pub(crate) fn apply_epoch_and_dry_run( ) -> TransactionBuilder { builder .with_min_epoch(intent.min_epoch.map(Epoch)) - .with_max_epoch(intent.max_epoch.map(Epoch)) .with_dry_run(intent.dry_run) } @@ -122,7 +125,7 @@ pub fn build_public_transfer_unsigned( intent: &PublicTransferIntent, ) -> Result { let recipe = resolve_transfer_recipe(intent)?; - let mut builder = build_transfer_recipe_builder(network, &recipe); + let mut builder = build_transfer_recipe_builder(network, &recipe, Epoch(intent.max_epoch)); // Attach the caller-supplied explicit inputs. let inputs = intent.inputs_to_internal()?; @@ -162,7 +165,7 @@ mod tests { fee: BoundaryAmount::new(2000), inputs: vec![InputRef::versioned(component_str(), 0)], min_epoch: Some(5), - max_epoch: Some(99), + max_epoch: 99, dry_run: false, } } @@ -179,7 +182,7 @@ mod tests { // Epochs + dry-run threaded through. assert_eq!(unsigned.min_epoch(), Some(Epoch(5))); - assert_eq!(unsigned.max_epoch(), Some(Epoch(99))); + assert_eq!(unsigned.max_epoch(), Epoch(99)); // The explicit input was added. assert!(unsigned.inputs().iter().any(|i| i.version() == Some(0))); diff --git a/crates/ootle_sdk_core/src/cosign.rs b/crates/ootle_sdk_core/src/cosign.rs index 9892a894ed..1cc059f92f 100644 --- a/crates/ootle_sdk_core/src/cosign.rs +++ b/crates/ootle_sdk_core/src/cosign.rs @@ -289,7 +289,7 @@ mod tests { fee: BoundaryAmount::new(2000), inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, } } diff --git a/crates/ootle_sdk_core/src/faucet.rs b/crates/ootle_sdk_core/src/faucet.rs index 430ed27fab..5142242451 100644 --- a/crates/ootle_sdk_core/src/faucet.rs +++ b/crates/ootle_sdk_core/src/faucet.rs @@ -49,9 +49,9 @@ pub struct FaucetClaimIntent { /// Optional earliest epoch this transaction is valid in. #[serde(default)] pub min_epoch: Option, - /// Optional latest epoch this transaction is valid in. - #[serde(default)] - pub max_epoch: Option, + /// The last epoch this transaction is valid in. Mandatory: every transaction has a bounded + /// validity window, capped network-wide at `max_transaction_validity_epochs` past the current epoch. + pub max_epoch: u64, /// Whether this is a dry run (e.g. fee estimation). #[serde(default)] pub dry_run: bool, @@ -158,7 +158,7 @@ mod tests { recipient_public_key: recipient_pk(), fee: BoundaryAmount::new(2000), min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, } } diff --git a/crates/ootle_sdk_core/src/generic_builder.rs b/crates/ootle_sdk_core/src/generic_builder.rs index 037dae1090..c674909563 100644 --- a/crates/ootle_sdk_core/src/generic_builder.rs +++ b/crates/ootle_sdk_core/src/generic_builder.rs @@ -118,7 +118,8 @@ fn lower_intent(network: Network, intent: &GenericTransactionIntent) -> Result Result Result Result, OotleSdkError> { - let mut builder = TransactionBuilder::new(network.as_byte()); + let mut builder = TransactionBuilder::new(network.as_byte(), Epoch(intent.max_epoch)); let mut bound_labels: HashSet = HashSet::new(); for instr in &intent.fee_instructions { builder = lower_instruction(builder, intent, instr, &mut bound_labels)?; @@ -581,7 +581,7 @@ mod tests { inputs, extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, } } @@ -713,7 +713,7 @@ mod tests { inputs: vec![InputRef::versioned(from_component().to_string(), 0)], extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; let (partial, _w) = build_unsigned_instructions_with_wants(Network::Esmeralda, &intent).unwrap(); @@ -756,7 +756,7 @@ mod tests { inputs: vec![InputRef::versioned(from_component().to_string(), 0)], extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; let (partial, _w) = build_unsigned_instructions_with_wants(Network::Esmeralda, &intent).unwrap(); @@ -790,7 +790,7 @@ mod tests { inputs: vec![InputRef::versioned(from_component().to_string(), 0)], extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; let err = build_unsigned_instructions_with_wants(Network::Esmeralda, &intent).unwrap_err(); @@ -811,7 +811,7 @@ mod tests { inputs: vec![InputRef::versioned(from_component().to_string(), 0)], extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; let err = build_unsigned_instructions_with_wants(Network::Esmeralda, &intent).unwrap_err(); @@ -849,7 +849,7 @@ mod tests { // Mirror the generic path exactly: inputs are carried ONLY by `new_with_explicit_inputs` // (the generic `lower_intent` never calls `with_inputs`), so the builder emits just the // instructions and the explicit inputs are folded in at seal time. - let unsigned = TransactionBuilder::new(Network::Esmeralda.as_byte()) + let unsigned = TransactionBuilder::new(Network::Esmeralda.as_byte(), Epoch(1)) .pay_fee_from_component(from_component(), Amount::new(2000)) .call_method(from_component(), "withdraw", args![resource(), Amount::new(1_000_000)]) .put_last_instruction_output_on_workspace("bucket") @@ -950,7 +950,7 @@ mod tests { inputs: vec![InputRef::versioned(faucet_component().to_string(), 0)], extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, } } @@ -1043,7 +1043,7 @@ mod tests { inputs: vec![InputRef::versioned(from_component().to_string(), 0)], extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; let wants = derive_wants(&intent).unwrap(); @@ -1094,7 +1094,7 @@ mod tests { inputs: vec![InputRef::versioned(from_component().to_string(), 0)], extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; let (partial, _w) = build_unsigned_instructions_with_wants(Network::Esmeralda, &intent).unwrap(); @@ -1130,7 +1130,7 @@ mod tests { inputs: vec![InputRef::versioned(from_component().to_string(), 0)], extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; let err = build_unsigned_instructions_with_wants(Network::Esmeralda, &intent).unwrap_err(); @@ -1159,7 +1159,7 @@ mod tests { // Auto-fill side: the two main instructions on the synchronous builder (no fee instruction, so // its inputs are exactly the component + the two literal-arg resources). - let auto_inputs: HashSet = TransactionBuilder::new(Network::Esmeralda.as_byte()) + let auto_inputs: HashSet = TransactionBuilder::new(Network::Esmeralda.as_byte(), Epoch(1)) .with_auto_fill_inputs() .call_method(foreign_component(), "do", args![arg_resource()]) .call_function(ACCOUNT_TEMPLATE_ADDRESS, "make", args![resource()]) @@ -1188,7 +1188,7 @@ mod tests { inputs: vec![], extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; let (partial, _w) = build_unsigned_instructions_with_wants(Network::Esmeralda, &intent).unwrap(); diff --git a/crates/ootle_sdk_core/src/inputs.rs b/crates/ootle_sdk_core/src/inputs.rs index 94a3f18ea4..ea0eaab7ad 100644 --- a/crates/ootle_sdk_core/src/inputs.rs +++ b/crates/ootle_sdk_core/src/inputs.rs @@ -451,7 +451,7 @@ pub fn build_public_transfer_unsigned_with_wants( intent: &PublicTransferIntent, ) -> Result<(PartialTransaction, WantList), OotleSdkError> { let recipe = resolve_transfer_recipe(intent)?; - let builder = build_transfer_recipe_builder(network, &recipe); + let builder = build_transfer_recipe_builder(network, &recipe, tari_ootle_common_types::Epoch(intent.max_epoch)); let builder = apply_epoch_and_dry_run(builder, intent); let unsigned = builder.build_unsigned(); @@ -960,7 +960,7 @@ mod tests { fee: BoundaryAmount::new(2000), inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, } } diff --git a/crates/ootle_sdk_core/src/public_transfer.rs b/crates/ootle_sdk_core/src/public_transfer.rs index 891b086616..33ef25a51e 100644 --- a/crates/ootle_sdk_core/src/public_transfer.rs +++ b/crates/ootle_sdk_core/src/public_transfer.rs @@ -100,7 +100,7 @@ mod tests { fee: BoundaryAmount::new(2000), inputs: vec![InputRef::versioned(component_str(), 0)], min_epoch: Some(5), - max_epoch: Some(99), + max_epoch: 99, dry_run: false, } } diff --git a/crates/ootle_sdk_core/src/resolved_transfer.rs b/crates/ootle_sdk_core/src/resolved_transfer.rs index c479cdeb5b..11fcd42605 100644 --- a/crates/ootle_sdk_core/src/resolved_transfer.rs +++ b/crates/ootle_sdk_core/src/resolved_transfer.rs @@ -187,7 +187,7 @@ mod tests { fee: BoundaryAmount::new(2000), inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, } } diff --git a/crates/ootle_sdk_core/src/stealth/assemble.rs b/crates/ootle_sdk_core/src/stealth/assemble.rs index b60a3645a6..042073f900 100644 --- a/crates/ootle_sdk_core/src/stealth/assemble.rs +++ b/crates/ootle_sdk_core/src/stealth/assemble.rs @@ -190,7 +190,7 @@ fn build_unsigned_transaction( let fee = intent.fee.to_internal(); let revealed_amount = intent.revealed_input_amount; - let mut builder = TransactionBuilder::new(network.as_byte()); + let mut builder = TransactionBuilder::new(network.as_byte(), tari_ootle_common_types::Epoch(intent.max_epoch)); builder = builder.pay_fee_from_component(from_component, fee); builder = if revealed_amount > 0 { @@ -239,7 +239,6 @@ fn build_unsigned_transaction( // Epoch window + dry-run flag. builder = builder .with_min_epoch(intent.min_epoch.map(tari_ootle_common_types::Epoch)) - .with_max_epoch(intent.max_epoch.map(tari_ootle_common_types::Epoch)) .with_dry_run(intent.dry_run); // Attach the resolved input substates (the from-account component + its vault, resolved via the @@ -386,7 +385,8 @@ fn seed_partial( // `build_unsigned_transaction` once the inputs/outputs statements exist (see // `assemble_resolved_stealth`). We only need the partial's resolver machinery + accumulated stealth // state + the stashed build context, so an empty unsigned suffices. - let unsigned = TransactionBuilder::new(network.as_byte()).build_unsigned(); + let unsigned = + TransactionBuilder::new(network.as_byte(), tari_ootle_common_types::Epoch(intent.max_epoch)).build_unsigned(); let ctx = StealthBuildCtx { intent: intent.clone(), entropy: entropy.clone(), @@ -668,7 +668,7 @@ mod tests { revealed_input_amount: revealed_input, revealed_output_amount: 0, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, } @@ -731,7 +731,7 @@ mod tests { revealed_input_amount: 500_000, revealed_output_amount: 500_000, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; @@ -817,7 +817,7 @@ mod tests { revealed_input_amount: 0, revealed_output_amount: 0, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; @@ -1017,7 +1017,7 @@ mod tests { revealed_input_amount: 0, revealed_output_amount: 0, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; @@ -1127,7 +1127,7 @@ mod tests { revealed_input_amount: 0, revealed_output_amount: 0, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: true, }; @@ -1217,7 +1217,7 @@ mod tests { revealed_input_amount: 0, revealed_output_amount: 0, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; @@ -1341,7 +1341,7 @@ mod tests { revealed_input_amount: 0, revealed_output_amount: 0, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; @@ -1427,7 +1427,7 @@ mod tests { revealed_input_amount: 1_500_000, revealed_output_amount: 500_000, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; @@ -1469,7 +1469,7 @@ mod tests { revealed_input_amount: 2_500_000, revealed_output_amount: 1_500_000, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; @@ -1518,7 +1518,7 @@ mod tests { revealed_input_amount: 1_500_000, revealed_output_amount: 500_000, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; diff --git a/crates/ootle_sdk_core/src/stealth/canonicalize.rs b/crates/ootle_sdk_core/src/stealth/canonicalize.rs index 0aa8ffa1a7..70ed3fc460 100644 --- a/crates/ootle_sdk_core/src/stealth/canonicalize.rs +++ b/crates/ootle_sdk_core/src/stealth/canonicalize.rs @@ -131,7 +131,7 @@ mod tests { fee: BoundaryAmount::new(2000), inputs: vec![InputRef::versioned(component, 0)], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; let keys = PublicTransferKeys::new(SecretKeyBytes::from_array(scalar_bytes(11))); diff --git a/crates/ootle_sdk_core/src/stealth/inputs.rs b/crates/ootle_sdk_core/src/stealth/inputs.rs index 1ea5009005..730abb9b50 100644 --- a/crates/ootle_sdk_core/src/stealth/inputs.rs +++ b/crates/ootle_sdk_core/src/stealth/inputs.rs @@ -722,7 +722,7 @@ mod tests { revealed_input_amount: 0, revealed_output_amount: 0, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; @@ -770,7 +770,7 @@ mod tests { revealed_input_amount: 0, revealed_output_amount: 0, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; diff --git a/crates/ootle_sdk_core/src/stealth/outputs.rs b/crates/ootle_sdk_core/src/stealth/outputs.rs index b286ca05fc..7bb1be11e5 100644 --- a/crates/ootle_sdk_core/src/stealth/outputs.rs +++ b/crates/ootle_sdk_core/src/stealth/outputs.rs @@ -394,7 +394,7 @@ mod tests { revealed_input_amount: 0, revealed_output_amount: revealed_output, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, } diff --git a/crates/ootle_sdk_core/src/stealth/sign_seal.rs b/crates/ootle_sdk_core/src/stealth/sign_seal.rs index f201c7e663..399b787fa0 100644 --- a/crates/ootle_sdk_core/src/stealth/sign_seal.rs +++ b/crates/ootle_sdk_core/src/stealth/sign_seal.rs @@ -492,7 +492,7 @@ mod tests { revealed_input_amount: 0, revealed_output_amount: 0, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; @@ -515,7 +515,7 @@ mod tests { revealed_input_amount: 1_000_000, revealed_output_amount: 0, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; diff --git a/crates/ootle_sdk_core/src/types/generic_intent.rs b/crates/ootle_sdk_core/src/types/generic_intent.rs index dd2c38dc9b..0df8ca9066 100644 --- a/crates/ootle_sdk_core/src/types/generic_intent.rs +++ b/crates/ootle_sdk_core/src/types/generic_intent.rs @@ -455,8 +455,9 @@ pub struct GenericTransactionIntent { pub extra_inputs: Vec, /// Optional earliest epoch this transaction is valid in. pub min_epoch: Option, - /// Optional latest epoch this transaction is valid in. - pub max_epoch: Option, + /// The last epoch this transaction is valid in. Mandatory: every transaction has a bounded + /// validity window, capped network-wide at `max_transaction_validity_epochs` past the current epoch. + pub max_epoch: u64, /// Whether this is a dry run. pub dry_run: bool, } @@ -959,7 +960,7 @@ mod tests { inputs: vec![], extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; assert_eq!(intent.validate_blob_indices().unwrap_err().code(), "VALIDATION"); @@ -1018,7 +1019,7 @@ mod tests { inputs: vec![InputRef::versioned(component_str(), 0)], extra_inputs: vec![], min_epoch: Some(1), - max_epoch: Some(10), + max_epoch: 10, dry_run: true, }; let json = serde_json::to_string(&intent).unwrap(); diff --git a/crates/ootle_sdk_core/src/types/intent.rs b/crates/ootle_sdk_core/src/types/intent.rs index 8b11fd0134..c782a7e1c9 100644 --- a/crates/ootle_sdk_core/src/types/intent.rs +++ b/crates/ootle_sdk_core/src/types/intent.rs @@ -96,8 +96,9 @@ pub struct PublicTransferIntent { pub inputs: Vec, /// Optional earliest epoch this transaction is valid in. pub min_epoch: Option, - /// Optional latest epoch this transaction is valid in. - pub max_epoch: Option, + /// The last epoch this transaction is valid in. Mandatory: every transaction has a bounded + /// validity window, capped network-wide at `max_transaction_validity_epochs` past the current epoch. + pub max_epoch: u64, /// Whether this is a dry run. The core sets `is_seal_signer_authorized` itself — it is /// intentionally **not** part of the intent. pub dry_run: bool, @@ -159,7 +160,7 @@ mod tests { InputRef::unversioned(resource_str()), ], min_epoch: Some(10), - max_epoch: None, + max_epoch: 1, dry_run: false, }; let reqs = intent.inputs_to_internal().unwrap(); @@ -180,7 +181,7 @@ mod tests { fee: BoundaryAmount::new(2000), inputs: vec![InputRef::unversioned(resource_str())], min_epoch: None, - max_epoch: Some(99), + max_epoch: 99, dry_run: true, }; let json = serde_json::to_string(&intent).unwrap(); diff --git a/crates/ootle_sdk_core/src/types/stealth.rs b/crates/ootle_sdk_core/src/types/stealth.rs index 5ee0db1675..ac3346150e 100644 --- a/crates/ootle_sdk_core/src/types/stealth.rs +++ b/crates/ootle_sdk_core/src/types/stealth.rs @@ -312,8 +312,9 @@ pub struct StealthTransferIntent { pub revealed_output_amount: u64, /// Optional earliest epoch. pub min_epoch: Option, - /// Optional latest epoch. - pub max_epoch: Option, + /// The last epoch this transaction is valid in. Mandatory: every transaction has a bounded + /// validity window, capped network-wide at `max_transaction_validity_epochs` past the current epoch. + pub max_epoch: u64, /// Dry-run flag. pub dry_run: bool, /// Pay the fee from the account's revealed (XTR) vault even when there is no revealed input. @@ -648,7 +649,7 @@ mod tests { revealed_input_amount: 0, revealed_output_amount: 0, min_epoch: None, - max_epoch: Some(99), + max_epoch: 99, dry_run: false, pay_fee_from_revealed: false, }; @@ -753,7 +754,7 @@ mod tests { revealed_input_amount: 0, revealed_output_amount, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, } diff --git a/crates/ootle_sdk_core/tests/golden_vectors.rs b/crates/ootle_sdk_core/tests/golden_vectors.rs index 03378fd411..0e11b9e863 100644 --- a/crates/ootle_sdk_core/tests/golden_vectors.rs +++ b/crates/ootle_sdk_core/tests/golden_vectors.rs @@ -169,7 +169,7 @@ fn sample_input() -> VectorInput { fee: BoundaryAmount::new(2000), inputs: vec![InputRef::versioned(component, 0)], min_epoch: Some(5), - max_epoch: Some(99), + max_epoch: 99, dry_run: false, }; @@ -221,7 +221,7 @@ fn single_key_basic_input() -> VectorInput { fee: BoundaryAmount::new(2500), inputs: vec![InputRef::versioned(component, 0)], min_epoch: Some(1), - max_epoch: Some(10), + max_epoch: 10, dry_run: false, }; @@ -357,7 +357,7 @@ fn resolve_single_key_basic_input() -> VectorInput { // No explicit inputs ⇒ the resolution path runs. inputs: vec![], min_epoch: Some(1), - max_epoch: Some(10), + max_epoch: 10, dry_run: false, }; @@ -676,7 +676,7 @@ fn stealth_intent(amount: u64, with_view: bool) -> ootle_sdk_core::types::stealt revealed_input_amount: 0, revealed_output_amount: 0, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, } @@ -885,7 +885,7 @@ fn stealth_send_stealth_seal_seed() -> Fixture { revealed_input_amount: 0, revealed_output_amount: 0, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; @@ -914,7 +914,7 @@ fn stealth_send_account_key_seed() -> Fixture { revealed_input_amount: 1_000_000, revealed_output_amount: 0, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; @@ -943,7 +943,7 @@ fn stealth_send_revealed_output_single_seed() -> Fixture { revealed_input_amount: 1_500_000, revealed_output_amount: 500_000, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; @@ -975,7 +975,7 @@ fn stealth_send_revealed_output_multi_seed() -> Fixture { revealed_input_amount: 2_500_000, revealed_output_amount: 1_500_000, min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, pay_fee_from_revealed: false, }; @@ -1941,7 +1941,7 @@ fn generic_transfer_intent() -> GenericTransactionIntent { inputs: generic_explicit_inputs(), extra_inputs: vec![], min_epoch: Some(1), - max_epoch: Some(10), + max_epoch: 10, dry_run: false, } } @@ -1964,7 +1964,7 @@ fn generic_create_account_seed() -> Fixture { inputs: generic_explicit_inputs(), extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; generic_build_fixture("generic_build/create_account", intent) @@ -1986,7 +1986,7 @@ fn generic_call_function_seed() -> Fixture { inputs: generic_explicit_inputs(), extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; generic_build_fixture("generic_build/call_function", intent) @@ -2007,7 +2007,7 @@ fn generic_publish_template_seed() -> Fixture { inputs: generic_explicit_inputs(), extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; generic_build_fixture("generic_build/publish_template", intent) @@ -2040,7 +2040,7 @@ fn generic_workspace_pipe_seed() -> Fixture { inputs: generic_explicit_inputs(), extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; generic_build_fixture("generic_build/workspace_pipe", intent) @@ -2078,7 +2078,7 @@ fn generic_self_funding_faucet_seed() -> Fixture { inputs: generic_explicit_inputs(), extra_inputs: vec![], min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; generic_build_fixture("generic_build/self_funding_faucet", intent) @@ -2111,7 +2111,7 @@ fn faucet_claim_seed() -> Fixture { recipient_public_key: generic_recipient_pk(), fee: BoundaryAmount::new(2000), min_epoch: None, - max_epoch: None, + max_epoch: 1, dry_run: false, }; @@ -2161,7 +2161,7 @@ fn generic_seals_identically_to_hand_built() { // carried ONLY by `new_with_explicit_inputs` (the generic path also never calls `with_inputs`), // so the comparison is like-for-like. let explicit_input = generic_explicit_inputs()[0].to_internal().unwrap(); - let unsigned = TransactionBuilder::new(Network::Esmeralda.as_byte()) + let unsigned = TransactionBuilder::new(Network::Esmeralda.as_byte(), tari_ootle_common_types::Epoch(10)) .pay_fee_from_component(generic_from_component(), Amount::new(2500)) .call_method(generic_from_component(), "withdraw", args![ generic_resource(), @@ -2170,7 +2170,6 @@ fn generic_seals_identically_to_hand_built() { .put_last_instruction_output_on_workspace("bucket") .create_account(generic_recipient_pk().to_internal()) .with_min_epoch(Some(tari_ootle_common_types::Epoch(1))) - .with_max_epoch(Some(tari_ootle_common_types::Epoch(10))) .build_unsigned(); let partial = PartialTransaction::new_with_explicit_inputs(unsigned, vec![explicit_input]); let hand = match apply_fetched_substates(partial, &[]).unwrap() { @@ -2284,7 +2283,7 @@ fn cosign_input() -> VectorInput { fee: BoundaryAmount::new(2500), inputs: vec![], min_epoch: Some(1), - max_epoch: Some(10), + max_epoch: 10, dry_run: false, }; VectorInput { diff --git a/crates/ootle_sdk_core/tests/ootle_rs_crosscheck.rs b/crates/ootle_sdk_core/tests/ootle_rs_crosscheck.rs index 541a323d0e..17c5513b35 100644 --- a/crates/ootle_sdk_core/tests/ootle_rs_crosscheck.rs +++ b/crates/ootle_sdk_core/tests/ootle_rs_crosscheck.rs @@ -82,7 +82,7 @@ fn build_unsigned_via_shared_builder(network: u8, intent: &PublicTransferIntent) }; // --- ootle-rs recipe, verbatim (account.rs). --- - let mut builder = TransactionBuilder::new(network); + let mut builder = TransactionBuilder::new(network, tari_ootle_common_types::Epoch(intent.max_epoch)); // `pay_fee` → pay_fee_from_component on the from-component. builder = builder.pay_fee_from_component(from_component, fee); @@ -102,7 +102,6 @@ fn build_unsigned_via_shared_builder(network: u8, intent: &PublicTransferIntent) builder = builder .with_min_epoch(intent.min_epoch.map(tari_ootle_common_types::Epoch)) - .with_max_epoch(intent.max_epoch.map(tari_ootle_common_types::Epoch)) .with_dry_run(intent.dry_run); builder.build_unsigned() diff --git a/crates/ootle_wasm/core/src/hash.rs b/crates/ootle_wasm/core/src/hash.rs index 5a70ddb840..f008d87f69 100644 --- a/crates/ootle_wasm/core/src/hash.rs +++ b/crates/ootle_wasm/core/src/hash.rs @@ -47,6 +47,7 @@ mod tests { ristretto::{RistrettoPublicKey, RistrettoSecretKey}, tari_utilities::ByteArray, }; + use tari_engine_types::Epoch; use tari_ootle_transaction::UnsignedTransactionV1; use super::*; @@ -58,7 +59,7 @@ mod tests { let public_key_bytes = public_key.as_bytes().to_vec(); let seal_signer_bytes: RistrettoPublicKeyBytes = public_key.to_byte_type(); - let tx = UnsignedTransactionV1::new(0u8, vec![], vec![], Default::default(), None, None, false); + let tx = UnsignedTransactionV1::new(0u8, vec![], vec![], Default::default(), None, Epoch(1), false); // Hash via our function let our_hash = hash_unsigned_transaction(&tx, &public_key_bytes).unwrap(); @@ -75,7 +76,7 @@ mod tests { let public_key = RistrettoPublicKey::from_secret_key(&secret); let public_key_bytes = public_key.as_bytes().to_vec(); - let tx = UnsignedTransactionV1::new(0u8, vec![], vec![], Default::default(), None, None, false); + let tx = UnsignedTransactionV1::new(0u8, vec![], vec![], Default::default(), None, Epoch(1), false); let json = serde_json::to_string(&tx).unwrap(); let hash_from_struct = hash_unsigned_transaction(&tx, &public_key_bytes).unwrap(); diff --git a/crates/ootle_wasm/core/src/transaction.rs b/crates/ootle_wasm/core/src/transaction.rs index f877b09bc3..9b8e681151 100644 --- a/crates/ootle_wasm/core/src/transaction.rs +++ b/crates/ootle_wasm/core/src/transaction.rs @@ -60,12 +60,13 @@ mod tests { ristretto::{RistrettoPublicKey, RistrettoSecretKey}, tari_utilities::ByteArray, }; + use tari_engine_types::Epoch; use tari_ootle_transaction::{Transaction, UnsignedTransactionV1}; use super::*; fn make_unsigned_tx() -> UnsignedTransactionV1 { - UnsignedTransactionV1::new(0u8, vec![], vec![], Default::default(), None, None, false) + UnsignedTransactionV1::new(0u8, vec![], vec![], Default::default(), None, Epoch(1), false) } #[test] diff --git a/crates/state_store_rocksdb/src/writer.rs b/crates/state_store_rocksdb/src/writer.rs index 08141e53af..7ce1d10d75 100644 --- a/crates/state_store_rocksdb/src/writer.rs +++ b/crates/state_store_rocksdb/src/writer.rs @@ -859,7 +859,7 @@ impl<'tx, TAddr: NodeAddressable + 'tx> StateStoreWriteTransaction for RocksDbSt initial_evidence: &Evidence, is_ready: bool, is_global: bool, - max_epoch: Option, + max_epoch: Epoch, transaction_weight: u64, ) -> Result<(), StorageError> { let value = TransactionPoolRecord::load( diff --git a/crates/state_store_rocksdb/tests/transactions.rs b/crates/state_store_rocksdb/tests/transactions.rs index 4aa02ff9f1..cdf0cf9d53 100644 --- a/crates/state_store_rocksdb/tests/transactions.rs +++ b/crates/state_store_rocksdb/tests/transactions.rs @@ -86,11 +86,11 @@ mod confirm_all_transitions { block1.as_locked().set(&mut tx).unwrap(); block1.as_leaf().set(&mut tx).unwrap(); - tx.transaction_pool_insert_new(atom1.id, atom1.decision, &Evidence::empty(), true, false, None, 0) + tx.transaction_pool_insert_new(atom1.id, atom1.decision, &Evidence::empty(), true, false, Epoch(1), 0) .unwrap(); - tx.transaction_pool_insert_new(atom2.id, atom2.decision, &Evidence::empty(), true, false, None, 0) + tx.transaction_pool_insert_new(atom2.id, atom2.decision, &Evidence::empty(), true, false, Epoch(1), 0) .unwrap(); - tx.transaction_pool_insert_new(atom3.id, atom3.decision, &Evidence::empty(), true, false, None, 0) + tx.transaction_pool_insert_new(atom3.id, atom3.decision, &Evidence::empty(), true, false, Epoch(1), 0) .unwrap(); let block_id = *block1.id(); let transactions = tx.transaction_pool_get_all(1000).unwrap(); @@ -191,7 +191,7 @@ mod confirm_all_transitions { block1.as_locked().set(&mut tx).unwrap(); block1.as_leaf().set(&mut tx).unwrap(); - tx.transaction_pool_insert_new(atom1.id, atom1.decision, &Evidence::empty(), true, false, None, 0) + tx.transaction_pool_insert_new(atom1.id, atom1.decision, &Evidence::empty(), true, false, Epoch(1), 0) .unwrap(); // Base record has no pending update yet. @@ -230,21 +230,21 @@ mod transaction_operations { // transactions_insert let tx1 = TransactionRecord::new( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .add_instruction(Instruction::DropAllProofsInWorkspace) .add_input(SubstateRequirement::new(create_random_substate_id(), Some(0))) .build_and_seal(&PrivateKey::default()), ); tx.transactions_insert(&tx1).unwrap(); let tx2 = TransactionRecord::new( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .add_instruction(Instruction::DropAllProofsInWorkspace) .add_input(SubstateRequirement::new(create_random_substate_id(), Some(1))) .build_and_seal(&PrivateKey::default()), ); tx.transactions_insert(&tx2).unwrap(); let unexisting_tx = TransactionRecord::new( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .add_instruction(Instruction::DropAllProofsInWorkspace) .add_input(SubstateRequirement::new(create_random_substate_id(), Some(2))) .build_and_seal(&PrivateKey::default()), @@ -267,7 +267,7 @@ mod transaction_operations { // transactions_update let updated_tx = TransactionRecord::new( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .add_instruction(Instruction::DropAllProofsInWorkspace) .add_input(SubstateRequirement::new(create_random_substate_id(), Some(3))) .build_and_seal(&PrivateKey::default()), @@ -308,14 +308,14 @@ mod transaction_execution_operations { // insert some transactions let tx1 = TransactionRecord::new( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .add_instruction(Instruction::DropAllProofsInWorkspace) .add_input(SubstateRequirement::new(create_random_substate_id(), Some(0))) .build_and_seal(&PrivateKey::default()), ); tx.transactions_insert(&tx1).unwrap(); let tx2 = TransactionRecord::new( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .add_instruction(Instruction::DropAllProofsInWorkspace) .add_input(SubstateRequirement::new(create_random_substate_id(), Some(1))) .build_and_seal(&PrivateKey::default()), @@ -408,8 +408,16 @@ mod transaction_execution_operations { assert!(all.is_empty()); // transactions_finalize_all - tx.transaction_pool_insert_new(*tx1.id(), Decision::Commit, &Evidence::empty(), true, false, None, 0) - .unwrap(); + tx.transaction_pool_insert_new( + *tx1.id(), + Decision::Commit, + &Evidence::empty(), + true, + false, + Epoch(1), + 0, + ) + .unwrap(); let transactions = tx.transaction_pool_get_all(1000).unwrap(); assert_eq!(transactions.len(), 1); tx.transactions_finalize_all(Epoch(1), transactions.iter()).unwrap(); @@ -446,7 +454,7 @@ mod transaction_execution_operations { let mut tx = db.create_write_tx().unwrap(); let tx1 = TransactionRecord::new( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .add_instruction(Instruction::DropAllProofsInWorkspace) .add_input(SubstateRequirement::new(create_random_substate_id(), Some(0))) .build_and_seal(&PrivateKey::default()), @@ -505,7 +513,7 @@ mod finalized_transaction_gc { fn insert_transaction(tx: &mut impl StateStoreWriteTransaction) -> TransactionRecord { let rec = TransactionRecord::new( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .add_instruction(Instruction::DropAllProofsInWorkspace) .add_input(SubstateRequirement::new(create_random_substate_id(), Some(0))) .build_and_seal(&PrivateKey::default()), @@ -527,8 +535,16 @@ mod finalized_transaction_gc { commit_chain(&mut tx, &chain); let tx1 = insert_transaction(&mut tx); - tx.transaction_pool_insert_new(*tx1.id(), Decision::Commit, &Evidence::empty(), true, false, None, 0) - .unwrap(); + tx.transaction_pool_insert_new( + *tx1.id(), + Decision::Commit, + &Evidence::empty(), + true, + false, + Epoch(1), + 0, + ) + .unwrap(); let pool = tx.transaction_pool_get_all(1000).unwrap(); tx.transactions_finalize_all(Epoch(1), pool.iter()).unwrap(); assert!(tx.transactions_exists(tx1.id()).unwrap()); @@ -561,8 +577,16 @@ mod finalized_transaction_gc { commit_chain(&mut tx, &chain); let tx1 = insert_transaction(&mut tx); - tx.transaction_pool_insert_new(*tx1.id(), Decision::Commit, &Evidence::empty(), true, false, None, 0) - .unwrap(); + tx.transaction_pool_insert_new( + *tx1.id(), + Decision::Commit, + &Evidence::empty(), + true, + false, + Epoch(1), + 0, + ) + .unwrap(); let pool = tx.transaction_pool_get_all(1000).unwrap(); tx.transactions_finalize_all(Epoch(1), pool.iter()).unwrap(); @@ -591,8 +615,16 @@ mod finalized_transaction_gc { commit_chain(&mut tx, &chain); let tx1 = insert_transaction(&mut tx); - tx.transaction_pool_insert_new(*tx1.id(), Decision::Commit, &Evidence::empty(), true, false, None, 0) - .unwrap(); + tx.transaction_pool_insert_new( + *tx1.id(), + Decision::Commit, + &Evidence::empty(), + true, + false, + Epoch(1), + 0, + ) + .unwrap(); let pool = tx.transaction_pool_get_all(1000).unwrap(); // Finalized in epoch 1, then finalized again in epoch 5: the index entry moves. @@ -670,8 +702,16 @@ mod get_many_ready_weight_budget { block1.as_leaf().set(&mut tx).unwrap(); for (atom, weight) in atoms.iter().zip(weights) { - tx.transaction_pool_insert_new(atom.id, atom.decision, &Evidence::empty(), true, false, None, *weight) - .unwrap(); + tx.transaction_pool_insert_new( + atom.id, + atom.decision, + &Evidence::empty(), + true, + false, + Epoch(1), + *weight, + ) + .unwrap(); } let block_id = *block1.id(); tx.commit().unwrap(); diff --git a/crates/storage/src/consensus_models/transaction_pool.rs b/crates/storage/src/consensus_models/transaction_pool.rs index b304498ced..7ea2eac94a 100644 --- a/crates/storage/src/consensus_models/transaction_pool.rs +++ b/crates/storage/src/consensus_models/transaction_pool.rs @@ -73,7 +73,7 @@ impl TransactionPool { initial_evidence: &Evidence, is_ready: bool, is_global: bool, - max_epoch: Option, + max_epoch: Epoch, transaction_weight: u64, ) -> Result<(), TransactionPoolError> { tx.transaction_pool_insert_new( @@ -416,7 +416,7 @@ pub struct TransactionPoolRecord { /// The maximum epoch for which this transaction is valid. #[serde(default)] #[n(11)] - max_epoch: Option, + max_epoch: Epoch, /// Epoch to use when executing the transaction. This updates as foreign proposals are received /// until the transaction is executed. #[n(12)] @@ -478,7 +478,7 @@ impl TransactionPoolRecord { local_decision: Option, remote_decision: Option, is_ready: bool, - max_epoch: Option, + max_epoch: Epoch, locked_epoch: Option, last_updated: time::OffsetDateTime, last_updated_in_block: Option, @@ -551,7 +551,7 @@ impl TransactionPoolRecord { self.remote_decision } - pub fn max_epoch(&self) -> Option { + pub fn max_epoch(&self) -> Epoch { self.max_epoch } @@ -1063,7 +1063,7 @@ mod tests { local_decision: None, remote_decision: None, is_ready: true, - max_epoch: None, + max_epoch: Epoch(1), locked_epoch: None, last_updated: time::OffsetDateTime::now_utc(), last_updated_in_block: None, @@ -1128,7 +1128,7 @@ mod tests { local_decision: None, remote_decision: None, is_ready: false, - max_epoch: None, + max_epoch: Epoch(1), locked_epoch: None, last_updated: time::OffsetDateTime::now_utc(), last_updated_in_block: None, diff --git a/crates/storage/src/state_store/mod.rs b/crates/storage/src/state_store/mod.rs index ecd6c5474a..3304092b9c 100644 --- a/crates/storage/src/state_store/mod.rs +++ b/crates/storage/src/state_store/mod.rs @@ -477,7 +477,7 @@ pub trait StateStoreWriteTransaction { initial_evidence: &Evidence, is_ready: bool, is_global: bool, - max_epoch: Option, + max_epoch: Epoch, transaction_weight: u64, ) -> Result<(), StorageError>; fn transaction_pool_add_pending_update( diff --git a/crates/template_builtin/tests/liquidity_pool.rs b/crates/template_builtin/tests/liquidity_pool.rs index 03c6979452..4334e930eb 100644 --- a/crates/template_builtin/tests/liquidity_pool.rs +++ b/crates/template_builtin/tests/liquidity_pool.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use tari_engine_types::indexed_value::IndexedWellKnownTypes; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{AccessRule, OwnerRule, constants::TARI_TOKEN, metadata}; use tari_template_lib_types::{Amount, ComponentAddress, ResourceAddress}; use tari_template_test_tooling::TemplateTest; @@ -26,7 +26,7 @@ fn initial_contribution_and_redeem() { // ACT 1: Create liquidity pool and contribute liquidity test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("pool") .call_function(template_address, "create", args![ OwnerRule::OwnedBySigner, @@ -80,7 +80,7 @@ fn initial_contribution_and_redeem() { // ACT 2: Redeem liquidity test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(user1, "withdraw", args![pool_unit_resx, pool_units.balance()]) .put_last_instruction_output_on_workspace("redeem_pool_units") .call_method(pool_addr, "redeem", args![Workspace("redeem_pool_units")]) @@ -119,7 +119,7 @@ fn second_contribution_and_partial_redeem() { // ACT 1: create the pool with an initial 1000 TARI : 4000 stablecoin contribution (ratio 1:4, LP minted = 2000). test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("pool") .call_function(template_address, "create", args![ OwnerRule::OwnedBySigner, @@ -159,7 +159,7 @@ fn second_contribution_and_partial_redeem() { // ACT 2: a deliberately non-proportional second contribution: 500 TARI but 4000 stablecoin. At the 1:4 reserve // ratio only 2000 stablecoin is needed to match the 500 TARI, so 2000 stablecoin must be returned as change. test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet_component, "take_free_coins_custom", args![4000]) .put_last_instruction_output_on_workspace("faucet_coins") .call_method(user1, "withdraw", args![TARI_TOKEN, 500]) @@ -200,7 +200,7 @@ fn second_contribution_and_partial_redeem() { // ACT 3: redeem HALF the LP (1500 of 3000). Proportional payout = 750 TARI and 3000 stablecoin. test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(user1, "withdraw", args![lp_resx, 1500]) .put_last_instruction_output_on_workspace("redeem_lp") .call_method(pool_addr, "redeem", args![Workspace("redeem_lp")]) @@ -242,7 +242,7 @@ fn bootstrap_contribution_after_seeding_reserve() { // ACT 1: create the pool owned by user1 (so user1 can call the owner-gated protected_* methods). With // `OwnerRule::OwnedBySigner` the owner is the seal signer, i.e. user1. test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("pool") .call_function(template_address, "create", args![ OwnerRule::OwnedBySigner, @@ -265,7 +265,7 @@ fn bootstrap_contribution_after_seeding_reserve() { // ACT 2: owner seeds 1000 TARI into reserve A via protected_add_liquidity (no LP minted). test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(user1, "withdraw", args![TARI_TOKEN, 1000]) .put_last_instruction_output_on_workspace("seed_a") .call_method(pool_addr, "protected_add_liquidity", args![Workspace("seed_a")]) @@ -276,7 +276,7 @@ fn bootstrap_contribution_after_seeding_reserve() { // ACT 3: bootstrap with a full contribution of 500 TARI + 2000 stablecoin. Total reserves become // (1000 + 500, 0 + 2000) = (1500, 2000) and LP minted = floor(sqrt(1500 * 2000)) = 1732. test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(faucet_component, "take_free_coins_custom", args![2000]) .put_last_instruction_output_on_workspace("faucet_coins") .call_method(user1, "withdraw", args![TARI_TOKEN, 500]) @@ -328,7 +328,7 @@ fn basic_constant_product_swap() { // ACT 1: Create liquidity pool and contribute liquidity test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("pool") .call_function(template_address, "create", args![ OwnerRule::OwnedBySigner, @@ -374,7 +374,7 @@ fn basic_constant_product_swap() { // ACT 2: Swap to pay fees test.enable_fees(); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .with_fee_instructions_builder(|builder| { // User2 would like to swap stablecoin for XTR to pay fees builder @@ -425,7 +425,7 @@ pub fn create_test_faucet_component>( ) -> (ComponentAddress, ResourceAddress) { let template_addr = test.get_template_address("TestFaucet"); let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(template_addr, "mint", args![initial_supply.into()]) .build_and_seal(test.secret_key()), vec![], diff --git a/crates/template_builtin/tests/nft_faucet.rs b/crates/template_builtin/tests/nft_faucet.rs index a22cff2983..9e31f14e95 100644 --- a/crates/template_builtin/tests/nft_faucet.rs +++ b/crates/template_builtin/tests/nft_faucet.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_engine_types::commit_result::ExecuteResult; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::{ ComponentAddress, Metadata, @@ -72,7 +72,7 @@ fn mint_faucet_nft( metadata: Metadata, ) -> ExecuteResult { test.build_and_execute( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(NFT_FAUCET_COMPONENT_ADDRESS, "mint", args![1, metadata]) .put_last_instruction_output_on_workspace("my_nft") .call_method(account, "deposit", args![Workspace("my_nft")]), diff --git a/crates/template_builtin/tests/xtr_faucet.rs b/crates/template_builtin/tests/xtr_faucet.rs index 68e3ead712..39a25f1743 100644 --- a/crates/template_builtin/tests/xtr_faucet.rs +++ b/crates/template_builtin/tests/xtr_faucet.rs @@ -1,7 +1,7 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_lib::types::constants::{STEALTH_TARI_RESOURCE_ADDRESS, XTR_FAUCET_COMPONENT_ADDRESS}; use tari_template_test_tooling::{TemplateTest, support::assert_error::assert_reject_reason}; @@ -39,7 +39,7 @@ fn second_claim_by_same_signer_is_rejected() { // Second claim: same signing key → same claim-receipt NFT ID → DuplicateNonFungibleId let reject_reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_method(XTR_FAUCET_COMPONENT_ADDRESS, "take", args![account]) .build_and_seal(&secret_key), vec![], diff --git a/crates/template_test_tooling/src/template_test.rs b/crates/template_test_tooling/src/template_test.rs index 7a91c3266f..b9bc28fa0c 100644 --- a/crates/template_test_tooling/src/template_test.rs +++ b/crates/template_test_tooling/src/template_test.rs @@ -796,7 +796,7 @@ impl TemplateTest { pub fn transaction(&self) -> TransactionBuilder { let seq = self.transaction_seq.get(); self.transaction_seq.set(seq + 1); - Transaction::builder(Network::LocalNet).with_max_epoch(Some(Epoch(seq))) + Transaction::builder(Network::LocalNet, Epoch(seq)) } /// Executes a transaction. Panics if the transaction is not finalized (fee transaction fails). Does not panic if diff --git a/crates/transaction/src/builder/mod.rs b/crates/transaction/src/builder/mod.rs index 61f7aeac9e..eeb6a036c7 100644 --- a/crates/transaction/src/builder/mod.rs +++ b/crates/transaction/src/builder/mod.rs @@ -79,13 +79,15 @@ pub struct TransactionBuilder { } impl TransactionBuilder { - pub fn new>(network: N) -> Self { + /// `max_epoch` is required: every transaction has a bounded validity window, so the last epoch + /// in which it may be sequenced must be chosen when it is built. + pub fn new>(network: N, max_epoch: Epoch) -> Self { let network = network.into(); Self { - unsigned_transaction: UnsignedTransaction::new(network), + unsigned_transaction: UnsignedTransaction::new(network, max_epoch), workspace_ids: WorkspaceIds::new(), blob_ids: BlobIds::new(), - fee_instruction_builder: Some(Box::new(Self::new_fee_builder(network))), + fee_instruction_builder: Some(Box::new(Self::new_fee_builder(network, max_epoch))), _discriminator: std::marker::PhantomData, fill_inputs: false, } @@ -94,7 +96,10 @@ impl TransactionBuilder { pub fn with_unsigned_transaction>(self, unsigned_transaction: T) -> Self { let unsigned_transaction = unsigned_transaction.into(); Self { - fee_instruction_builder: Some(Box::new(Self::new_fee_builder(unsigned_transaction.network()))), + fee_instruction_builder: Some(Box::new(Self::new_fee_builder( + unsigned_transaction.network(), + unsigned_transaction.max_epoch(), + ))), unsigned_transaction, workspace_ids: WorkspaceIds::new(), blob_ids: BlobIds::new(), @@ -103,9 +108,9 @@ impl TransactionBuilder { } } - fn new_fee_builder>(network: N) -> TransactionBuilder { + fn new_fee_builder>(network: N, max_epoch: Epoch) -> TransactionBuilder { TransactionBuilder { - unsigned_transaction: UnsignedTransaction::new(network), + unsigned_transaction: UnsignedTransaction::new(network, max_epoch), workspace_ids: WorkspaceIds::new(), blob_ids: BlobIds::new(), fee_instruction_builder: None, @@ -194,7 +199,7 @@ impl TransactionBuilder { self } - pub fn with_max_epoch(mut self, max_epoch: Option) -> Self { + pub fn with_max_epoch(mut self, max_epoch: Epoch) -> Self { self.unsigned_transaction.set_max_epoch(max_epoch); self } diff --git a/crates/transaction/src/builder/tests.rs b/crates/transaction/src/builder/tests.rs index 42411aadb2..ebf7e06108 100644 --- a/crates/transaction/src/builder/tests.rs +++ b/crates/transaction/src/builder/tests.rs @@ -1,6 +1,7 @@ // Copyright 2025 The Tari Project // SPDX-License-Identifier: BSD-3-Clause +use tari_ootle_common_types::Epoch; use tari_template_lib_types::TemplateAddress; use crate::{ @@ -15,7 +16,7 @@ use crate::{ #[test] fn it_converts_workspace_names_to_ids() { - let transaction = Transaction::builder_localnet() + let transaction = Transaction::builder_localnet(Epoch(1)) .put_last_instruction_output_on_workspace("thing1") .allocate_resource_address("thing2") .allocate_component_address("thing3") @@ -65,12 +66,12 @@ fn merge_remaps_blob_ids_and_appends_blobs() { let address = TemplateAddress::from_array([7; 32]); // First builder owns one blob `a` referenced by an arg. - let a = Transaction::builder_localnet() + let a = Transaction::builder_localnet(Epoch(1)) .add_blob("a", vec![1u8, 2, 3]) .call_function(address, "f", args![Blob("a")]); // Second builder owns its own blob `b`. - let b = Transaction::builder_localnet() + let b = Transaction::builder_localnet(Epoch(1)) .add_blob("b", vec![4u8, 5]) .call_function(address, "g", args![Blob("b")]); @@ -99,8 +100,8 @@ fn merge_remaps_blob_ids_and_appends_blobs() { #[test] #[should_panic(expected = "blob name 'a' collides during merge")] fn merge_rejects_colliding_blob_names() { - let a = Transaction::builder_localnet().add_blob("a", vec![1u8]); - let b = Transaction::builder_localnet().add_blob("a", vec![2u8]); + let a = Transaction::builder_localnet(Epoch(1)).add_blob("a", vec![1u8]); + let b = Transaction::builder_localnet(Epoch(1)).add_blob("a", vec![2u8]); let _unused = a.merge(b); } @@ -108,8 +109,8 @@ fn merge_rejects_colliding_blob_names() { fn merge_remaps_publish_template_blob_index() { // `self` already has a blob, so the merged builder's auto-added template blob index 0 // becomes index 1 after merge. - let a = Transaction::builder_localnet().add_blob("filler", vec![0u8; 4]); - let b = Transaction::builder_localnet().publish_template(vec![9u8, 9, 9]); + let a = Transaction::builder_localnet(Epoch(1)).add_blob("filler", vec![0u8; 4]); + let b = Transaction::builder_localnet(Epoch(1)).publish_template(vec![9u8, 9, 9]); let merged = a.merge(b).build_unsigned(); diff --git a/crates/transaction/src/lib.rs b/crates/transaction/src/lib.rs index eb73ece9b2..6cb1eff4f5 100644 --- a/crates/transaction/src/lib.rs +++ b/crates/transaction/src/lib.rs @@ -38,6 +38,9 @@ pub use builder::TransactionBuilder; pub use envelope::*; pub use ootle_network::Network; pub use signable::*; +// Re-exported because `max_epoch` is a mandatory public field of the unsigned transaction, so any +// caller constructing one needs the type. +pub use tari_ootle_common_types::Epoch; pub use transaction::*; pub use transaction_id::*; pub use unsealed::*; diff --git a/crates/transaction/src/transaction.rs b/crates/transaction/src/transaction.rs index 5c5f14788f..227635d041 100644 --- a/crates/transaction/src/transaction.rs +++ b/crates/transaction/src/transaction.rs @@ -54,12 +54,12 @@ impl Transaction { /// Creates a new transaction builder. /// NOTE: The network is set to LocalNet. Be sure to set the correct network using for_network. /// NOTE: this method will likely be deprecated in the future - pub fn builder_localnet() -> TransactionBuilder { - Self::builder(Network::LocalNet) + pub fn builder_localnet(max_epoch: Epoch) -> TransactionBuilder { + Self::builder(Network::LocalNet, max_epoch) } - pub fn builder>(network: N) -> TransactionBuilder { - TransactionBuilder::new(network) + pub fn builder>(network: N, max_epoch: Epoch) -> TransactionBuilder { + TransactionBuilder::new(network, max_epoch) } pub fn new(transaction: UnsealedTransaction, seal_signature: TransactionSealSignature) -> Self { @@ -290,7 +290,7 @@ impl Transaction { } } - pub fn max_epoch(&self) -> Option { + pub fn max_epoch(&self) -> Epoch { match self { Self::V1(tx) => tx.max_epoch(), } @@ -434,7 +434,7 @@ impl PrunedTransaction { } } - pub fn max_epoch(&self) -> Option { + pub fn max_epoch(&self) -> Epoch { match self { Self::V1(tx) => tx.max_epoch(), } @@ -483,7 +483,7 @@ mod tests { use crate::{args, call_args}; fn create_transaction() -> TransactionBuilder { - Transaction::builder(123u8) + Transaction::builder(123u8, Epoch(1)) .create_account(Default::default()) .call_method(ComponentAddress::from_array([1; 32]), "method", args![ 1, diff --git a/crates/transaction/src/unsigned_transaction.rs b/crates/transaction/src/unsigned_transaction.rs index 5353520a83..4cc87e1a2d 100644 --- a/crates/transaction/src/unsigned_transaction.rs +++ b/crates/transaction/src/unsigned_transaction.rs @@ -31,8 +31,8 @@ pub enum UnsignedTransaction { } impl UnsignedTransaction { - pub fn new>(network: N) -> Self { - Self::V1(UnsignedTransactionV1::new_default(network)) + pub fn new>(network: N, max_epoch: Epoch) -> Self { + Self::V1(UnsignedTransactionV1::new_default(network, max_epoch)) } pub fn then Self>(self, f: F) -> Self { @@ -128,7 +128,7 @@ impl UnsignedTransaction { } } - pub fn max_epoch(&self) -> Option { + pub fn max_epoch(&self) -> Epoch { match self { Self::V1(tx) => tx.max_epoch(), } @@ -158,7 +158,7 @@ impl UnsignedTransaction { self } - pub fn set_max_epoch(&mut self, max_epoch: Option) -> &mut Self { + pub fn set_max_epoch(&mut self, max_epoch: Epoch) -> &mut Self { match self { Self::V1(tx) => tx.max_epoch = max_epoch, } diff --git a/crates/transaction/src/v1/intent.rs b/crates/transaction/src/v1/intent.rs index 2f784c557a..4190cd2db7 100644 --- a/crates/transaction/src/v1/intent.rs +++ b/crates/transaction/src/v1/intent.rs @@ -126,7 +126,7 @@ mod tests { ], inputs, min_epoch: Some(Epoch(100)), - max_epoch: Some(Epoch(200)), + max_epoch: Epoch(200), is_seal_signer_authorized: false, dry_run: true, blobs: Blobs::from_vec(vec![Blob::from(vec![1, 2, 3])]), @@ -235,7 +235,7 @@ mod tests { .collect(); assert_ne!(commitment_of(u), base_commitment, "inputs (version changed)"); - // min_epoch / max_epoch: value change and Some <-> None + // min_epoch: value change and Some <-> None; max_epoch: value change let mut u = base.clone(); u.min_epoch = Some(Epoch(101)); assert_ne!(commitment_of(u), base_commitment, "min_epoch (value)"); @@ -243,11 +243,8 @@ mod tests { u.min_epoch = None; assert_ne!(commitment_of(u), base_commitment, "min_epoch (None)"); let mut u = base.clone(); - u.max_epoch = Some(Epoch(999)); + u.max_epoch = Epoch(999); assert_ne!(commitment_of(u), base_commitment, "max_epoch (value)"); - let mut u = base.clone(); - u.max_epoch = None; - assert_ne!(commitment_of(u), base_commitment, "max_epoch (None)"); // is_seal_signer_authorized let mut u = base.clone(); diff --git a/crates/transaction/src/v1/pruned.rs b/crates/transaction/src/v1/pruned.rs index 16e42150a5..a426bd3ce4 100644 --- a/crates/transaction/src/v1/pruned.rs +++ b/crates/transaction/src/v1/pruned.rs @@ -52,7 +52,7 @@ pub struct PrunedUnsignedTransactionV1 { #[n(4)] pub min_epoch: Option, #[n(5)] - pub max_epoch: Option, + pub max_epoch: Epoch, #[n(6)] pub is_seal_signer_authorized: bool, #[n(7)] @@ -263,7 +263,7 @@ impl PrunedTransactionV1 { self.body.transaction.min_epoch } - pub fn max_epoch(&self) -> Option { + pub fn max_epoch(&self) -> Epoch { self.body.transaction.max_epoch } @@ -383,7 +383,7 @@ mod tests { instructions: vec![Instruction::DropAllProofsInWorkspace], inputs, min_epoch: Some(Epoch(100)), - max_epoch: Some(Epoch(200)), + max_epoch: Epoch(200), is_seal_signer_authorized: false, dry_run: true, blobs, diff --git a/crates/transaction/src/v1/signature.rs b/crates/transaction/src/v1/signature.rs index 3eccf1793c..f78e1423b7 100644 --- a/crates/transaction/src/v1/signature.rs +++ b/crates/transaction/src/v1/signature.rs @@ -411,7 +411,7 @@ pub(crate) struct TransactionSignatureFields<'a> { instructions: &'a [Instruction], inputs: &'a IndexSet, min_epoch: Option, - max_epoch: Option, + max_epoch: Epoch, is_seal_signer_authorized: bool, dry_run: bool, nonce: u64, @@ -483,7 +483,7 @@ mod tests { ], inputs, min_epoch: Some(Epoch(100)), - max_epoch: Some(Epoch(200)), + max_epoch: Epoch(200), is_seal_signer_authorized: false, dry_run: true, blobs: crate::Blobs::empty(), @@ -576,13 +576,10 @@ mod tests { tx.min_epoch = None; assert_ne!(sig_msg(&signer, &tx), base_msg, "min_epoch (None)"); - // max_epoch: value change / Some <-> None + // max_epoch let mut tx = base.clone(); - tx.max_epoch = Some(Epoch(999)); + tx.max_epoch = Epoch(999); assert_ne!(sig_msg(&signer, &tx), base_msg, "max_epoch (value)"); - let mut tx = base.clone(); - tx.max_epoch = None; - assert_ne!(sig_msg(&signer, &tx), base_msg, "max_epoch (None)"); // is_seal_signer_authorized let mut tx = base.clone(); @@ -707,11 +704,8 @@ mod tests { // max_epoch let mut u = base_unsigned.clone(); - u.max_epoch = Some(Epoch(999)); + u.max_epoch = Epoch(999); assert_ne!(seal_msg(&with_body(u)), base_msg, "max_epoch (value)"); - let mut u = base_unsigned.clone(); - u.max_epoch = None; - assert_ne!(seal_msg(&with_body(u)), base_msg, "max_epoch (None)"); // is_seal_signer_authorized let mut u = base_unsigned.clone(); diff --git a/crates/transaction/src/v1/transaction.rs b/crates/transaction/src/v1/transaction.rs index 6122461774..3b5d46fae6 100644 --- a/crates/transaction/src/v1/transaction.rs +++ b/crates/transaction/src/v1/transaction.rs @@ -220,7 +220,7 @@ impl TransactionV1 { self.body.min_epoch() } - pub fn max_epoch(&self) -> Option { + pub fn max_epoch(&self) -> Epoch { self.body.max_epoch() } @@ -458,7 +458,7 @@ mod blob_validation_tests { instructions, inputs: indexmap::IndexSet::new(), min_epoch: None, - max_epoch: None, + max_epoch: Epoch(1), is_seal_signer_authorized: true, dry_run: false, blobs, @@ -616,7 +616,7 @@ mod transaction_id_tests { instructions: vec![Instruction::PutLastInstructionOutputOnWorkspace { key: 3 }], inputs: indexmap::IndexSet::new(), min_epoch: None, - max_epoch: Some(Epoch(10)), + max_epoch: Epoch(10), is_seal_signer_authorized: true, dry_run: false, blobs: Blobs::empty(), diff --git a/crates/transaction/src/v1/unsealed.rs b/crates/transaction/src/v1/unsealed.rs index 09e0fa735b..33f8c4a040 100644 --- a/crates/transaction/src/v1/unsealed.rs +++ b/crates/transaction/src/v1/unsealed.rs @@ -142,7 +142,7 @@ impl UnsealedTransactionV1 { self.transaction.min_epoch } - pub fn max_epoch(&self) -> Option { + pub fn max_epoch(&self) -> Epoch { self.transaction.max_epoch } diff --git a/crates/transaction/src/v1/unsigned.rs b/crates/transaction/src/v1/unsigned.rs index 5256d0fac5..b22ca367ab 100644 --- a/crates/transaction/src/v1/unsigned.rs +++ b/crates/transaction/src/v1/unsigned.rs @@ -30,8 +30,12 @@ pub struct UnsignedTransactionV1 { pub inputs: IndexSet, #[n(4)] pub min_epoch: Option, + /// The last epoch in which this transaction may be sequenced. Mandatory: every transaction has a + /// bounded lifetime, capped at `ConsensusConstants::max_transaction_validity_epochs` past the + /// current epoch, so a transaction's death is deterministic and an aborted attempt cannot be + /// retried indefinitely. #[n(5)] - pub max_epoch: Option, + pub max_epoch: Epoch, #[n(6)] pub is_seal_signer_authorized: bool, #[n(7)] @@ -61,14 +65,14 @@ pub struct UnsignedTransactionV1 { } impl UnsignedTransactionV1 { - pub(crate) fn new_default>(network: N) -> Self { + pub(crate) fn new_default>(network: N, max_epoch: Epoch) -> Self { Self { network: network.into(), fee_instructions: vec![], instructions: vec![], inputs: IndexSet::new(), min_epoch: None, - max_epoch: None, + max_epoch, is_seal_signer_authorized: true, dry_run: false, blobs: Blobs::empty(), @@ -82,7 +86,7 @@ impl UnsignedTransactionV1 { instructions: Vec, inputs: IndexSet, min_epoch: Option, - max_epoch: Option, + max_epoch: Epoch, dry_run: bool, ) -> Self { Self { @@ -173,7 +177,7 @@ impl UnsignedTransactionV1 { self.min_epoch } - pub fn max_epoch(&self) -> Option { + pub fn max_epoch(&self) -> Epoch { self.max_epoch } diff --git a/crates/transaction_validation/src/dry_run.rs b/crates/transaction_validation/src/dry_run.rs index 6349906506..587538f962 100644 --- a/crates/transaction_validation/src/dry_run.rs +++ b/crates/transaction_validation/src/dry_run.rs @@ -32,6 +32,7 @@ impl Validator for TransactionDryRunValidator { #[cfg(test)] mod tests { use indexmap::IndexSet; + use tari_ootle_common_types::Epoch; use tari_ootle_transaction::{ Network, Transaction, @@ -53,7 +54,7 @@ mod tests { vec![], IndexSet::new(), None, - None, + Epoch(1), dry_run, ), vec![TransactionSignature::new( diff --git a/crates/transaction_validation/src/epoch_range.rs b/crates/transaction_validation/src/epoch_range.rs index b1d240f676..f217d7e580 100644 --- a/crates/transaction_validation/src/epoch_range.rs +++ b/crates/transaction_validation/src/epoch_range.rs @@ -9,12 +9,20 @@ use crate::{TransactionValidationError, Validator}; const LOG_TARGET: &str = "tari::ootle::mempool::validators::epoch_range"; -#[derive(Debug, Default)] -pub struct EpochRangeValidator; +/// Validates a transaction's validity window against the current epoch. +/// +/// `max_validity_epochs` is the consensus-uniform ceiling on how far ahead of the current epoch a +/// transaction's `max_epoch` may sit. Bounding it is what makes a transaction's death deterministic: +/// past `max_epoch` the transaction can never be sequenced, so a wallet can declare it dead and an +/// aborted attempt cannot be retried forever. +#[derive(Debug)] +pub struct EpochRangeValidator { + max_validity_epochs: u64, +} impl EpochRangeValidator { - pub fn new() -> Self { - Self + pub fn new(max_validity_epochs: u64) -> Self { + Self { max_validity_epochs } } } @@ -33,9 +41,8 @@ impl Validator for EpochRangeValidator { }); } - if let Some(max_epoch) = transaction.max_epoch() && - current_epoch > max_epoch - { + let max_epoch = transaction.max_epoch(); + if current_epoch > max_epoch { warn!(target: LOG_TARGET, "EpochRangeValidator - FAIL: Current epoch {current_epoch} greater than maximum epoch {max_epoch}."); return Err(TransactionValidationError::CurrentEpochGreaterThanMaximum { current_epoch, @@ -43,6 +50,110 @@ impl Validator for EpochRangeValidator { }); } + // Saturating: an overflowing ceiling admits every representable max_epoch, which is the + // correct reading of "no epoch is further ahead than the limit allows". + let latest_permitted = Epoch(current_epoch.as_u64().saturating_add(self.max_validity_epochs)); + if max_epoch > latest_permitted { + warn!( + target: LOG_TARGET, + "EpochRangeValidator - FAIL: Maximum epoch {max_epoch} is more than {} epochs beyond current epoch \ + {current_epoch}.", + self.max_validity_epochs + ); + return Err(TransactionValidationError::MaxEpochTooFarAhead { + current_epoch, + max_epoch, + max_validity_epochs: self.max_validity_epochs, + }); + } + Ok(()) } } + +#[cfg(test)] +mod tests { + use indexmap::IndexSet; + use tari_ootle_common_types::Epoch; + use tari_ootle_transaction::{ + Network, + Transaction, + TransactionSealSignature, + UnsealedTransactionV1, + UnsignedTransactionV1, + }; + use tari_template_lib::types::crypto::{RistrettoPublicKeyBytes, SchnorrSignatureBytes}; + + use super::*; + + const MAX_VALIDITY_EPOCHS: u64 = 10; + + fn transaction(min_epoch: Option, max_epoch: Epoch) -> Transaction { + Transaction::new( + UnsealedTransactionV1::new( + UnsignedTransactionV1::new( + Network::LocalNet.as_byte(), + vec![], + vec![], + IndexSet::new(), + min_epoch, + max_epoch, + false, + ), + vec![], + ) + .into(), + TransactionSealSignature::new(RistrettoPublicKeyBytes::zero(), SchnorrSignatureBytes::zero()), + ) + } + + fn validate(current_epoch: Epoch, transaction: &Transaction) -> Result<(), TransactionValidationError> { + EpochRangeValidator::new(MAX_VALIDITY_EPOCHS).validate(¤t_epoch, transaction) + } + + #[test] + fn it_accepts_a_transaction_inside_its_window() { + let tx = transaction(Some(Epoch(5)), Epoch(12)); + validate(Epoch(5), &tx).unwrap(); + validate(Epoch(12), &tx).unwrap(); + } + + #[test] + fn it_rejects_a_transaction_before_its_min_epoch() { + let tx = transaction(Some(Epoch(5)), Epoch(12)); + assert!(matches!( + validate(Epoch(4), &tx), + Err(TransactionValidationError::CurrentEpochLessThanMinimum { .. }) + )); + } + + #[test] + fn it_rejects_an_expired_transaction() { + let tx = transaction(None, Epoch(12)); + assert!(matches!( + validate(Epoch(13), &tx), + Err(TransactionValidationError::CurrentEpochGreaterThanMaximum { .. }) + )); + } + + #[test] + fn it_rejects_a_window_beyond_the_ceiling() { + let tx = transaction(None, Epoch(11)); + assert!(matches!( + validate(Epoch(0), &tx), + Err(TransactionValidationError::MaxEpochTooFarAhead { .. }) + )); + } + + #[test] + fn the_ceiling_is_inclusive() { + let tx = transaction(None, Epoch(MAX_VALIDITY_EPOCHS)); + validate(Epoch(0), &tx).unwrap(); + } + + #[test] + fn a_ceiling_that_overflows_admits_any_max_epoch() { + let tx = transaction(None, Epoch(u64::MAX)); + EpochRangeValidator::new(u64::MAX).validate(&Epoch(1), &tx).unwrap(); + } +} diff --git a/crates/transaction_validation/src/error.rs b/crates/transaction_validation/src/error.rs index 7a48fdd91c..d9afeb419d 100644 --- a/crates/transaction_validation/src/error.rs +++ b/crates/transaction_validation/src/error.rs @@ -32,6 +32,15 @@ pub enum TransactionValidationError { CurrentEpochLessThanMinimum { current_epoch: Epoch, min_epoch: Epoch }, #[error("Current epoch ({current_epoch}) is greater than maximum epoch ({max_epoch}) required for transaction")] CurrentEpochGreaterThanMaximum { current_epoch: Epoch, max_epoch: Epoch }, + #[error( + "Maximum epoch ({max_epoch}) is more than {max_validity_epochs} epochs beyond the current epoch \ + ({current_epoch})" + )] + MaxEpochTooFarAhead { + current_epoch: Epoch, + max_epoch: Epoch, + max_validity_epochs: u64, + }, #[error("Invalid transaction signature")] InvalidSignature, #[error("Transaction {transaction_id} has no main signer")] @@ -108,7 +117,8 @@ impl TransactionValidationError { Self::OutputSubstateExists { .. } | Self::ValidatorFeeClaimEpochInvalid { .. } | Self::CurrentEpochLessThanMinimum { .. } | - Self::CurrentEpochGreaterThanMaximum { .. } => false, + Self::CurrentEpochGreaterThanMaximum { .. } | + Self::MaxEpochTooFarAhead { .. } => false, // Properties of the transaction itself, on which every node agrees. Self::NoFeeInstructions { .. } | @@ -157,6 +167,11 @@ mod tests { current_epoch: Epoch(3), max_epoch: Epoch(2), }, + TransactionValidationError::MaxEpochTooFarAhead { + current_epoch: Epoch(1), + max_epoch: Epoch(100), + max_validity_epochs: 10, + }, ]; for err in node_local { assert!(!err.is_sender_fault(), "must not penalise the sender for: {err}"); diff --git a/crates/transaction_validation/src/network.rs b/crates/transaction_validation/src/network.rs index 380bea7c39..0539ee6838 100644 --- a/crates/transaction_validation/src/network.rs +++ b/crates/transaction_validation/src/network.rs @@ -45,6 +45,7 @@ impl Validator for TransactionNetworkValidator { #[cfg(test)] mod tests { use indexmap::IndexSet; + use tari_ootle_common_types::Epoch; use tari_ootle_transaction::{ Network, Transaction, @@ -60,7 +61,7 @@ mod tests { fn tx(network_byte: u8) -> Transaction { Transaction::new( UnsealedTransactionV1::new( - UnsignedTransactionV1::new(network_byte, vec![], vec![], IndexSet::new(), None, None, false), + UnsignedTransactionV1::new(network_byte, vec![], vec![], IndexSet::new(), None, Epoch(1), false), vec![TransactionSignature::new( RistrettoPublicKeyBytes::zero(), SchnorrSignatureBytes::zero(), diff --git a/crates/transaction_validation/src/publish_template_limits.rs b/crates/transaction_validation/src/publish_template_limits.rs index 762916b4e0..e459ec5611 100644 --- a/crates/transaction_validation/src/publish_template_limits.rs +++ b/crates/transaction_validation/src/publish_template_limits.rs @@ -57,6 +57,7 @@ impl Validator for PublishTemplateLimitValidator { #[cfg(test)] mod tests { use indexmap::IndexSet; + use tari_ootle_common_types::Epoch; use tari_ootle_transaction::{ Network, TransactionSealSignature, @@ -84,7 +85,7 @@ mod tests { instructions, IndexSet::new(), None, - None, + Epoch(1), false, ), vec![TransactionSignature::new( diff --git a/crates/transaction_validation/src/signature_limits.rs b/crates/transaction_validation/src/signature_limits.rs index dddec709d1..7631a3ea75 100644 --- a/crates/transaction_validation/src/signature_limits.rs +++ b/crates/transaction_validation/src/signature_limits.rs @@ -49,6 +49,7 @@ impl Validator for SignatureLimitValidator { #[cfg(test)] mod tests { use indexmap::IndexSet; + use tari_ootle_common_types::Epoch; use tari_ootle_transaction::{ Network, TransactionSealSignature, @@ -69,7 +70,7 @@ mod tests { vec![], IndexSet::new(), None, - None, + Epoch(1), false, ), (0..num_signatures) diff --git a/crates/transaction_validation/src/stealth_limits.rs b/crates/transaction_validation/src/stealth_limits.rs index 4602254bac..27f277731b 100644 --- a/crates/transaction_validation/src/stealth_limits.rs +++ b/crates/transaction_validation/src/stealth_limits.rs @@ -128,6 +128,7 @@ impl StealthTransactionLimitsValidator { #[cfg(test)] mod tests { use indexmap::IndexSet; + use tari_ootle_common_types::Epoch; use tari_ootle_transaction::{ Network, ResourceAddressRef, @@ -203,7 +204,7 @@ mod tests { transfer_instructions(main_statements), IndexSet::new(), None, - None, + Epoch(1), false, ), vec![TransactionSignature::new( diff --git a/crates/transaction_validation/src/weight.rs b/crates/transaction_validation/src/weight.rs index 004c8b7c43..19e65a5f33 100644 --- a/crates/transaction_validation/src/weight.rs +++ b/crates/transaction_validation/src/weight.rs @@ -53,6 +53,7 @@ impl Validator for TransactionWeightValidator { #[cfg(test)] mod tests { use indexmap::IndexSet; + use tari_ootle_common_types::Epoch; use tari_ootle_transaction::{ Instruction, Network, @@ -85,7 +86,7 @@ mod tests { instructions, IndexSet::new(), None, - None, + Epoch(1), false, ), vec![TransactionSignature::new( diff --git a/crates/wallet/ledger/app/src/handlers/sign_transaction.rs b/crates/wallet/ledger/app/src/handlers/sign_transaction.rs index e20a9f2d0c..ef47fe4998 100644 --- a/crates/wallet/ledger/app/src/handlers/sign_transaction.rs +++ b/crates/wallet/ledger/app/src/handlers/sign_transaction.rs @@ -193,7 +193,8 @@ fn capture_display(display: &mut TxDisplay, field: SigningField, data: &[u8]) -> SigningField::Inputs => display.input_count = read_u32(data)?, // Option borsh: 1-byte tag, then a u64 little-endian if present. SigningField::MinEpoch => display.min_epoch = read_option_u64(data)?, - SigningField::MaxEpoch => display.max_epoch = read_option_u64(data)?, + // Epoch borsh: a bare u64 little-endian — max_epoch is mandatory, so there is no tag. + SigningField::MaxEpoch => display.max_epoch = read_u64(data)?, _ => {}, } Ok(()) @@ -208,6 +209,15 @@ fn read_u32(data: &[u8]) -> Result { Ok(u32::from_le_bytes(bytes)) } +fn read_u64(data: &[u8]) -> Result { + let bytes: [u8; 8] = data + .get(..8) + .ok_or_else(bad_request)? + .try_into() + .map_err(|_| bad_request())?; + Ok(u64::from_le_bytes(bytes)) +} + fn read_option_u64(data: &[u8]) -> Result, AppStatus> { match data.first() { Some(0) => Ok(None), @@ -267,9 +277,7 @@ pub fn review_fields(review: &SignReview) -> Vec<(String, String)> { if let Some(epoch) = review.display.min_epoch { fields.push(("Min epoch".to_string(), epoch.to_string())); } - if let Some(epoch) = review.display.max_epoch { - fields.push(("Max epoch".to_string(), epoch.to_string())); - } + fields.push(("Max epoch".to_string(), review.display.max_epoch.to_string())); fields.push(("Tx digest".to_string(), to_hex(&review.message))); fields } diff --git a/crates/wallet/ledger/app/src/state.rs b/crates/wallet/ledger/app/src/state.rs index 4d05cc8192..e58c0afeff 100644 --- a/crates/wallet/ledger/app/src/state.rs +++ b/crates/wallet/ledger/app/src/state.rs @@ -47,5 +47,5 @@ pub struct TxDisplay { pub instruction_count: u32, pub input_count: u32, pub min_epoch: Option, - pub max_epoch: Option, + pub max_epoch: u64, } diff --git a/crates/wallet/ledger/client/tests/signing_recipe.rs b/crates/wallet/ledger/client/tests/signing_recipe.rs index 2c9df135e4..5713c8415f 100644 --- a/crates/wallet/ledger/client/tests/signing_recipe.rs +++ b/crates/wallet/ledger/client/tests/signing_recipe.rs @@ -33,6 +33,7 @@ use rand::Rng; use tari_ootle_transaction::{ Blob, Blobs, + Epoch, Instruction, PreimageSegment, TransactionSealSignature, @@ -178,7 +179,7 @@ fn sample_unsigned() -> UnsignedTransactionV1 { ], inputs: IndexSet::new(), min_epoch: None, - max_epoch: None, + max_epoch: Epoch(1), is_seal_signer_authorized: false, dry_run: true, blobs, diff --git a/crates/wallet/ootle-rs/examples/adaptor_claim.rs b/crates/wallet/ootle-rs/examples/adaptor_claim.rs index 23030cd2a7..3d7a122ab7 100644 --- a/crates/wallet/ootle-rs/examples/adaptor_claim.rs +++ b/crates/wallet/ootle-rs/examples/adaptor_claim.rs @@ -52,7 +52,7 @@ use tari_crypto::{ ristretto::{RistrettoPublicKey, RistrettoSecretKey}, }; use tari_ootle_common_types::engine_types::transaction_receipt::TransactionReceipt; -use tari_ootle_transaction::Transaction; +use tari_ootle_transaction::{Epoch, Transaction}; /// 1 TARI expressed in microTARI. const ONE_TARI: u64 = 1_000_000; @@ -75,6 +75,10 @@ async fn main() { .unwrap(); let current_epoch = provider.get_epoch().await.unwrap().as_u64(); + // Every transaction declares the last epoch it may be sequenced in; past it the transaction can + // never land. Ten epochs is a comfortable window for an example — the network caps how far + // ahead this may be set. + let max_epoch = Epoch(provider.get_epoch().await.unwrap().as_u64() + 10); println!("Current epoch: {current_epoch}"); let mut rng = rand::rng(); @@ -130,7 +134,7 @@ async fn main() { let swap_commitment = *lock_transfer.stealth_outputs()[0].commitment(); - let lock_tx = IFaucet::new(&provider) + let lock_tx = IFaucet::new(&provider, max_epoch) .take_faucet_funds() .into_stealth_transfer(lock_transfer) .and_pay_fee_from_revealed_output() @@ -168,7 +172,7 @@ async fn main() { .await .unwrap(); - let claim_tx = Transaction::builder(provider.network()) + let claim_tx = Transaction::builder(provider.network(), max_epoch) .with_fee_instructions_builder(|builder| { builder .stealth_transfer(tari_token, claim_transfer) diff --git a/crates/wallet/ootle-rs/examples/claim_burn.rs b/crates/wallet/ootle-rs/examples/claim_burn.rs index 7fd3ecdf88..8218bd61b3 100644 --- a/crates/wallet/ootle-rs/examples/claim_burn.rs +++ b/crates/wallet/ootle-rs/examples/claim_burn.rs @@ -45,6 +45,7 @@ use tari_crypto::{ ristretto::{RistrettoPublicKey, RistrettoSecretKey}, tari_utilities::hex::Hex, }; +use tari_ootle_transaction::Epoch; /// MicroTARI revealed from the claimed funds to pay the transaction fee. const MAX_FEE: u64 = 2000; @@ -98,10 +99,15 @@ async fn main() { .connect(default_indexer_url(network)) .await .expect("failed to connect to indexer"); + + // Every transaction declares the last epoch it may be sequenced in; past it the transaction can + // never land. Ten epochs is a comfortable window for an example — the network caps how far + // ahead this may be set. + let max_epoch = Epoch(provider.get_epoch().await.unwrap().as_u64() + 10); assert_eq!(provider.network(), network); // Build the claim transaction and the sealer that signs it with the derived stealth claim key. - let (unsigned_tx, sealer) = ClaimBurn::new(&provider, claim_proof, encrypted_data) + let (unsigned_tx, sealer) = ClaimBurn::new(&provider, claim_proof, encrypted_data, max_epoch) .with_max_fee(MAX_FEE) .with_memo_message("claimed via ootle-rs") .prepare() diff --git a/crates/wallet/ootle-rs/examples/fungible_transfer.rs b/crates/wallet/ootle-rs/examples/fungible_transfer.rs index 3def6ed384..fd15c67617 100644 --- a/crates/wallet/ootle-rs/examples/fungible_transfer.rs +++ b/crates/wallet/ootle-rs/examples/fungible_transfer.rs @@ -15,6 +15,7 @@ use ootle_rs::{ transaction::TransactionSigner, wallet::OotleWallet, }; +use tari_ootle_transaction::Epoch; #[tokio::main] async fn main() { @@ -58,9 +59,13 @@ async fn main() { // Get the latest block number. let latest_epoch = provider.get_epoch().await.unwrap(); println!("Latest epoch: {latest_epoch}"); + // Every transaction declares the last epoch it may be sequenced in; past it the transaction can + // never land. Ten epochs is a comfortable window for an example — the network caps how far + // ahead this may be set. + let max_epoch = Epoch(provider.get_epoch().await.unwrap().as_u64() + 10); // First let's transfer some faucet TARI to our account to have funds for fees and transfers. - let unsigned_tx = IFaucet::new(&provider) + let unsigned_tx = IFaucet::new(&provider, max_epoch) .take_faucet_funds() // NOTE that pay fee must be called after the faucet funds are taken because fees are paid from the faucet funds .pay_fee(500u64) @@ -87,7 +92,7 @@ async fn main() { // Send some TARI to another address. You can replace TARI_TOKEN with any other fungible token resource address. let tari_token = TARI_TOKEN; // resource_address!("resource_deadbeaf"); - let unsigned_tx = IAccount::new(&provider) + let unsigned_tx = IAccount::new(&provider, max_epoch) .pay_fee(1000u64) // Multiple transfers in a single transaction .public_transfer(&recipient1, tari_token, 2 * TARI) diff --git a/crates/wallet/ootle-rs/examples/stealth_spend_conditions.rs b/crates/wallet/ootle-rs/examples/stealth_spend_conditions.rs index a319c9123c..b61e6773bd 100644 --- a/crates/wallet/ootle-rs/examples/stealth_spend_conditions.rs +++ b/crates/wallet/ootle-rs/examples/stealth_spend_conditions.rs @@ -46,7 +46,7 @@ use ootle_rs::{ wallet::OotleWallet, }; use tari_ootle_common_types::engine_types::transaction_receipt::TransactionReceipt; -use tari_ootle_transaction::Transaction; +use tari_ootle_transaction::{Epoch, Transaction}; /// 1 TARI expressed in microTARI. const ONE_TARI: u64 = 1_000_000; @@ -68,6 +68,10 @@ async fn main() { .unwrap(); let current_epoch = provider.get_epoch().await.unwrap().as_u64(); + // Every transaction declares the last epoch it may be sequenced in; past it the transaction can + // never land. Ten epochs is a comfortable window for an example — the network caps how far + // ahead this may be set. + let max_epoch = Epoch(provider.get_epoch().await.unwrap().as_u64() + 10); println!("Current epoch: {current_epoch}"); // ------------------------------------------------------------------------------------------------- @@ -126,7 +130,7 @@ async fn main() { // The commitment of the HTLC output we are about to create — the input we will claim in TX 2. let htlc_commitment = *lock_transfer.stealth_outputs()[0].commitment(); - let lock_tx = IFaucet::new(&provider) + let lock_tx = IFaucet::new(&provider, max_epoch) .take_faucet_funds() .into_stealth_transfer(lock_transfer) .and_pay_fee_from_revealed_output() @@ -165,7 +169,7 @@ async fn main() { .await .unwrap(); - let claim_tx = Transaction::builder(provider.network()) + let claim_tx = Transaction::builder(provider.network(), max_epoch) .with_fee_instructions_builder(|builder| { builder .stealth_transfer(tari_token, claim_transfer) diff --git a/crates/wallet/ootle-rs/examples/stealth_transfer.rs b/crates/wallet/ootle-rs/examples/stealth_transfer.rs index 524c21cea7..cbd8e34d4b 100644 --- a/crates/wallet/ootle-rs/examples/stealth_transfer.rs +++ b/crates/wallet/ootle-rs/examples/stealth_transfer.rs @@ -20,7 +20,7 @@ use ootle_rs::{ wallet::OotleWallet, }; use tari_ootle_common_types::engine_types::transaction_receipt::TransactionReceipt; -use tari_ootle_transaction::Transaction; +use tari_ootle_transaction::{Epoch, Transaction}; #[tokio::main] async fn main() { @@ -59,6 +59,10 @@ async fn main() { assert_eq!(network, provider.network()); // Get the latest block number. let latest_epoch = provider.get_epoch().await.unwrap(); + // Every transaction declares the last epoch it may be sequenced in; past it the transaction can + // never land. Ten epochs is a comfortable window for an example — the network caps how far + // ahead this may be set. + let max_epoch = Epoch(provider.get_epoch().await.unwrap().as_u64() + 10); println!("Latest epoch: {latest_epoch}"); // Send some TARI to another address. You can replace TARI with any other fungible token resource address. @@ -97,7 +101,7 @@ async fn main() { let inputs_to_spend = faucet_transfer.stealth_outputs().to_vec(); // First let's transfer some faucet TARI to our account to have funds for fees and transfers. - let unsigned_tx = IFaucet::new(&provider) + let unsigned_tx = IFaucet::new(&provider, max_epoch) .take_faucet_funds() .into_stealth_transfer(faucet_transfer) .and_pay_fee_from_revealed_output() @@ -141,7 +145,7 @@ async fn main() { // We'll generate an unsigned transaction directly using the Transaction builder. In future, we may make this // easier. - let unsigned_tx = Transaction::builder(provider.network()) + let unsigned_tx = Transaction::builder(provider.network(), max_epoch) .with_fee_instructions_builder(|builder| { builder .stealth_transfer(tari_token, transfer) diff --git a/crates/wallet/ootle-rs/examples/template_invoke.rs b/crates/wallet/ootle-rs/examples/template_invoke.rs index 2182c89be4..7ab31fec79 100644 --- a/crates/wallet/ootle-rs/examples/template_invoke.rs +++ b/crates/wallet/ootle-rs/examples/template_invoke.rs @@ -31,13 +31,14 @@ use ootle_rs::{ wallet::OotleWallet, }; use tari_ootle_common_types::engine_types::published_template::PublishedTemplateAddress; +use tari_ootle_transaction::Epoch; // --------------------------------------------------------------------------- // Step 1: Define the template interface // // The macro generates a single generic struct `StableCoin<'a, P, I>` parameterized // by an interface marker. Use the constructors to select the interface: -// - `StableCoin::for_component(addr, &provider)` — component methods (&self / &mut self) -// - `StableCoin::for_template(addr, &provider)` — template functions (no self, e.g. constructors) +// - `StableCoin::for_component(addr, &provider, max_epoch)` — component methods (&self / &mut self) +// - `StableCoin::for_template(addr, &provider, max_epoch)` — template functions (no self, e.g. constructors) // // The method signatures should match the template's public API. Argument types // must implement `serde::Serialize` for CBOR encoding. @@ -109,8 +110,13 @@ async fn main() { .await .expect("Failed to connect to indexer"); + // Every transaction declares the last epoch it may be sequenced in; past it the transaction can + // never land. Ten epochs is a comfortable window for an example — the network caps how far + // ahead this may be set. + let max_epoch = Epoch(provider.get_epoch().await.unwrap().as_u64() + 10); + // Fund the account from faucet - let unsigned_tx = IFaucet::new(&provider) + let unsigned_tx = IFaucet::new(&provider, max_epoch) .take_faucet_funds() .pay_fee(500u64) .prepare() @@ -133,7 +139,7 @@ async fn main() { // ----------------------------------------------------------------------- let view_key = ootle_rs::template_types::crypto::RistrettoPublicKeyBytes::default(); - let tpl = StableCoin::for_template(stable_coin_template.as_template_address(), &provider); + let tpl = StableCoin::for_template(stable_coin_template.as_template_address(), &provider, max_epoch); println!("Stable coin template: {}", tpl.template_address()); let unsigned_tx = tpl @@ -159,7 +165,7 @@ async fn main() { // exposes methods with &self / &mut self. // ----------------------------------------------------------------------- - let coin = StableCoin::for_component(stable_coin_component, &provider); + let coin = StableCoin::for_component(stable_coin_component, &provider, max_epoch); println!("Stable coin component: {}", coin.component_address()); // -- Example: Increase supply -- @@ -182,7 +188,7 @@ async fn main() { wait_for_tx(&pending_tx).await; // -- Example: Set fee configuration -- - let coin = StableCoin::for_component(stable_coin_component, &provider); + let coin = StableCoin::for_component(stable_coin_component, &provider, max_epoch); let unsigned_tx = coin .set_config_transfer_fee_percentage(2u8) .pay_fee(1000u64) @@ -201,7 +207,7 @@ async fn main() { // -- Example: Using the generic IComponent builder directly -- // For one-off calls where defining a full interface isn't worth it, // you can use IComponent directly with string method names. - let unsigned_tx = IComponent::new(&provider) + let unsigned_tx = IComponent::new(&provider, max_epoch) .call_method(stable_coin_component, "decrease_supply", tari_ootle_transaction::args![ Amount::new(500_000) ]) @@ -220,7 +226,7 @@ async fn main() { // -- Example: Chaining with workspace piping -- // Withdraw returns a Bucket that can be piped to another method. - let coin = StableCoin::for_component(stable_coin_component, &provider); + let coin = StableCoin::for_component(stable_coin_component, &provider, max_epoch); let unsigned_tx = coin .withdraw(Amount::new(100_000)) .put_last_instruction_output_on_workspace("bucket") diff --git a/crates/wallet/ootle-rs/src/builtin_templates/account.rs b/crates/wallet/ootle-rs/src/builtin_templates/account.rs index a490ffee46..45f1e2cb0f 100644 --- a/crates/wallet/ootle-rs/src/builtin_templates/account.rs +++ b/crates/wallet/ootle-rs/src/builtin_templates/account.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; -use tari_ootle_common_types::SubstateRequirement; +use tari_ootle_common_types::{Epoch, SubstateRequirement}; use tari_ootle_template_metadata::MetadataHash; use tari_ootle_transaction::{Blob, TransactionBuilder, UnsignedTransaction, args}; use tari_template_lib_types::{Amount, ResourceAddress, constants::TARI_TOKEN}; @@ -59,10 +59,12 @@ impl<'a, P: Provider> UnsignedTransactionBuilder for AccountInvokeBuilder<'a, P> } impl<'a, P: Provider> AccountInvokeBuilder<'a, P> { - pub fn new(provider: &'a P) -> Self { + /// `max_epoch` is the last epoch the built transaction may be sequenced in. It is required: + /// every transaction carries a bounded validity window. + pub fn new(provider: &'a P, max_epoch: Epoch) -> Self { let network = provider.network(); Self { - builder: TransactionBuilder::new(network).with_auto_fill_inputs(), + builder: TransactionBuilder::new(network, max_epoch).with_auto_fill_inputs(), provider, want_list: HashSet::new(), } diff --git a/crates/wallet/ootle-rs/src/builtin_templates/component.rs b/crates/wallet/ootle-rs/src/builtin_templates/component.rs index 97deacaa13..61a1e2c264 100644 --- a/crates/wallet/ootle-rs/src/builtin_templates/component.rs +++ b/crates/wallet/ootle-rs/src/builtin_templates/component.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; -use tari_ootle_common_types::{SubstateRequirement, engine_types::substate::SubstateId}; +use tari_ootle_common_types::{Epoch, SubstateRequirement, engine_types::substate::SubstateId}; use tari_ootle_transaction::{TransactionBuilder, UnsignedTransaction, builder::named_args::NamedArg}; use tari_template_lib_types::{ Amount, @@ -181,10 +181,12 @@ impl<'a, P: Provider> TransactionBuildable for ComponentInvokeBuilder<'a, P> { } impl<'a, P: Provider> ComponentInvokeBuilder<'a, P> { - pub fn new(provider: &'a P) -> Self { + /// `max_epoch` is the last epoch the built transaction may be sequenced in. It is required: + /// every transaction carries a bounded validity window. + pub fn new(provider: &'a P, max_epoch: Epoch) -> Self { let network = provider.network(); Self { - builder: TransactionBuilder::new(network).with_auto_fill_inputs(), + builder: TransactionBuilder::new(network, max_epoch).with_auto_fill_inputs(), provider, want_list: HashSet::new(), } diff --git a/crates/wallet/ootle-rs/src/builtin_templates/faucet.rs b/crates/wallet/ootle-rs/src/builtin_templates/faucet.rs index f6e31bf532..fbdd029a7d 100644 --- a/crates/wallet/ootle-rs/src/builtin_templates/faucet.rs +++ b/crates/wallet/ootle-rs/src/builtin_templates/faucet.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; -use tari_ootle_common_types::SubstateRequirement; +use tari_ootle_common_types::{Epoch, SubstateRequirement}; use tari_ootle_transaction::{TransactionBuilder, UnsignedTransaction, args}; use tari_template_lib_types::{ Amount, @@ -73,10 +73,12 @@ impl<'a, P: Provider> UnsignedTransactionBuilder for FaucetInvokeBuilder<'a, P> } impl<'a, P: Provider> FaucetInvokeBuilder<'a, P> { - pub fn new(provider: &'a P) -> Self { + /// `max_epoch` is the last epoch the built transaction may be sequenced in. It is required: + /// every transaction carries a bounded validity window. + pub fn new(provider: &'a P, max_epoch: Epoch) -> Self { let network = provider.network(); Self { - builder: TransactionBuilder::new(network).with_auto_fill_inputs(), + builder: TransactionBuilder::new(network, max_epoch).with_auto_fill_inputs(), provider, want_list: HashSet::new(), account_workspace_name: None, diff --git a/crates/wallet/ootle-rs/src/claim_burn/mod.rs b/crates/wallet/ootle-rs/src/claim_burn/mod.rs index 8800d9854b..082e7d9bd9 100644 --- a/crates/wallet/ootle-rs/src/claim_burn/mod.rs +++ b/crates/wallet/ootle-rs/src/claim_burn/mod.rs @@ -46,7 +46,7 @@ use tari_crypto::{ // Re-export the burn proof types so callers don't need to reach into `tari_ootle_common_types`. pub use tari_ootle_common_types::engine_types::confidential::{ClaimBurnOutputData, MinotariBurnClaimProof}; use tari_ootle_common_types::engine_types::stealth::validate_transfer; -use tari_ootle_transaction::{Transaction, UnsealedTransaction, UnsignedTransaction}; +use tari_ootle_transaction::{Epoch, Transaction, UnsealedTransaction, UnsignedTransaction}; use tari_ootle_wallet_crypto::{StealthCryptoApi, memo::Memo}; use tari_template_lib_types::{Amount, EncryptedData, constants::TARI_TOKEN}; @@ -70,6 +70,7 @@ pub struct ClaimBurn<'a, P> { claim_proof: MinotariBurnClaimProof, encrypted_data: EncryptedData, max_fee: Amount, + max_epoch: Epoch, recipient: Option
, memo: Option, } @@ -79,12 +80,20 @@ impl<'a, P: Provider> ClaimBurn<'a, P> { /// /// `claim_proof` and `encrypted_data` are produced by the L1 (minotari) wallet for the burn /// output being claimed (the wallet daemon bundles them as `ClaimBurnProofContents`). - pub fn new(provider: &'a P, claim_proof: MinotariBurnClaimProof, encrypted_data: EncryptedData) -> Self { + /// `max_epoch` is the last epoch the claim transaction may be sequenced in. It is required: + /// every transaction carries a bounded validity window. + pub fn new( + provider: &'a P, + claim_proof: MinotariBurnClaimProof, + encrypted_data: EncryptedData, + max_epoch: Epoch, + ) -> Self { Self { provider, claim_proof, encrypted_data, max_fee: Amount::zero(), + max_epoch, recipient: None, memo: None, } @@ -133,6 +142,7 @@ impl<'a, P: WalletProvider> ClaimBurn<'a, P> { claim_proof, encrypted_data, max_fee, + max_epoch, recipient, memo, } = self; @@ -214,7 +224,7 @@ impl<'a, P: WalletProvider> ClaimBurn<'a, P> { // encrypted data here, so this is not strictly required, but it keeps the values consistent. let output_data = ClaimBurnOutputData { encrypted_data }; - let unsigned_tx = Transaction::builder(network) + let unsigned_tx = Transaction::builder(network, max_epoch) .with_fee_instructions_builder(|builder| { builder // Mint the burned funds as a confidential UTXO. diff --git a/crates/wallet/ootle-rs/src/key_provider/local/generic_impls.rs b/crates/wallet/ootle-rs/src/key_provider/local/generic_impls.rs index 48d22026f4..d247a8a1e9 100644 --- a/crates/wallet/ootle-rs/src/key_provider/local/generic_impls.rs +++ b/crates/wallet/ootle-rs/src/key_provider/local/generic_impls.rs @@ -577,7 +577,9 @@ mod tests { async fn the_derived_stealth_public_key_is_the_one_that_signs() { let provider = PrivateKeyProvider::random(Network::LocalNet); let (_, public_nonce) = RistrettoPublicKey::random_keypair(&mut rand::rng()); - let unsigned = tari_ootle_transaction::Transaction::builder(Network::LocalNet).build_unsigned(); + let unsigned = + tari_ootle_transaction::Transaction::builder(Network::LocalNet, tari_ootle_transaction::Epoch(1)) + .build_unsigned(); let derived = provider .stealth_public_key(&public_nonce) diff --git a/crates/wallet/ootle-rs/src/macros.rs b/crates/wallet/ootle-rs/src/macros.rs index 4cbaec4e77..f86a69e81b 100644 --- a/crates/wallet/ootle-rs/src/macros.rs +++ b/crates/wallet/ootle-rs/src/macros.rs @@ -9,7 +9,7 @@ macro_rules! resource_address { } pub mod _macro_exports { - pub use tari_ootle_common_types::{SubstateRequirement, engine_types::substate::SubstateId}; + pub use tari_ootle_common_types::{Epoch, SubstateRequirement, engine_types::substate::SubstateId}; pub use tari_ootle_transaction::{ self as transaction, TransactionBuilder, @@ -159,10 +159,11 @@ macro_rules! __ootle_template_inner { pub fn for_component( component: $crate::macros::_macro_exports::ComponentAddress, provider: &'a P, + max_epoch: $crate::macros::_macro_exports::Epoch, ) -> Self { Self { interface: $crate::macros::_macro_exports::ComponentInterface { component }, - builder: $crate::macros::_macro_exports::ComponentInvokeBuilder::new(provider), + builder: $crate::macros::_macro_exports::ComponentInvokeBuilder::new(provider, max_epoch), } } @@ -222,10 +223,11 @@ macro_rules! __ootle_template_inner { pub fn for_template( template: $crate::macros::_macro_exports::TemplateAddress, provider: &'a P, + max_epoch: $crate::macros::_macro_exports::Epoch, ) -> Self { Self { interface: $crate::macros::_macro_exports::TemplateInterface { template }, - builder: $crate::macros::_macro_exports::ComponentInvokeBuilder::new(provider), + builder: $crate::macros::_macro_exports::ComponentInvokeBuilder::new(provider, max_epoch), } } @@ -410,6 +412,7 @@ macro_rules! const_nonzero_u64 { #[cfg(test)] mod tests { + use tari_ootle_common_types::Epoch; use tari_template_lib_types::Amount; use crate::{Network, builtin_templates::component::TransactionBuildable}; @@ -496,13 +499,13 @@ mod tests { }; let component = tari_template_lib_types::ComponentAddress::new([0u8; 32].into()); - let coin = TestStableCoin::for_component(component, &provider); + let coin = TestStableCoin::for_component(component, &provider, Epoch(1)); // Verify component_address accessor assert_eq!(coin.component_address(), component); // Verify typed methods return Self and can be chained - let coin = TestStableCoin::for_component(component, &provider); + let coin = TestStableCoin::for_component(component, &provider, Epoch(1)); let coin = coin.increase_supply(Amount::new(1000)); // Can chain another typed method — this is the key improvement let coin = coin.decrease_supply(Amount::new(500)); @@ -531,11 +534,11 @@ mod tests { // Chain: StableCoin.withdraw -> put on workspace -> Account.deposit (via then, since // workspace refs don't cross chain boundaries) -> chain an independent Account.withdraw - let coin = TestStableCoin::for_component(component_a, &provider); + let coin = TestStableCoin::for_component(component_a, &provider, Epoch(1)); let _coin = coin .withdraw(Amount::new(1000)) .put_last_instruction_output_on_workspace("bucket") - .chain(TestAccount::for_component(component_b, &provider).withdraw(Amount::new(500))) + .chain(TestAccount::for_component(component_b, &provider, Epoch(1)).withdraw(Amount::new(500))) .pay_fee(1000u64); } @@ -549,13 +552,13 @@ mod tests { }; let template = tari_template_lib_types::TemplateAddress::from_array([1u8; 32]); - let tpl = TestStableCoin::for_template(template, &provider); + let tpl = TestStableCoin::for_template(template, &provider, Epoch(1)); // Verify template_address accessor assert_eq!(tpl.template_address(), template); // Verify template function returns Self and can chain shared methods - let tpl = TestStableCoin::for_template(template, &provider); + let tpl = TestStableCoin::for_template(template, &provider, Epoch(1)); let _tpl = tpl.instantiate(Amount::new(1_000_000)).pay_fee(1000u64); } } diff --git a/crates/wallet/ootle-rs/src/wallet/stealth.rs b/crates/wallet/ootle-rs/src/wallet/stealth.rs index 3947fb24ae..c6def8d887 100644 --- a/crates/wallet/ootle-rs/src/wallet/stealth.rs +++ b/crates/wallet/ootle-rs/src/wallet/stealth.rs @@ -139,6 +139,7 @@ mod tests { keys::{PublicKey, SecretKey}, ristretto::{RistrettoPublicKey, RistrettoSecretKey}, }; + use tari_ootle_common_types::Epoch; use super::*; use crate::{ @@ -168,7 +169,7 @@ mod tests { } fn unsigned() -> UnsignedTransaction { - Transaction::builder(Network::LocalNet).build_unsigned() + Transaction::builder(Network::LocalNet, Epoch(1)).build_unsigned() } /// Several stealth inputs owned by the same address: one seals with its one-time key and the rest authorize diff --git a/crates/wallet/sdk/src/apis/confidential_transfer.rs b/crates/wallet/sdk/src/apis/confidential_transfer.rs index 2271a2b518..76e71ec744 100644 --- a/crates/wallet/sdk/src/apis/confidential_transfer.rs +++ b/crates/wallet/sdk/src/apis/confidential_transfer.rs @@ -9,6 +9,7 @@ use tari_bor::{Deserialize, Serialize}; use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; use tari_ootle_address::OotleAddress; use tari_ootle_common_types::{ + Epoch, SubstateRequirement, optional::{IsNotFoundError, Optional}, }; @@ -429,7 +430,7 @@ where TSpec: WalletSdkSpec )?; let network = self.config_api.get_network()?; - let transaction = Transaction::builder(network.as_byte()) + let transaction = Transaction::builder(network.as_byte(), params.max_epoch) .with_dry_run(params.is_dry_run) // TODO: we assume that from_account has TARI .pay_fee_from_component(*from_account.component_address(), max_fee) @@ -537,6 +538,10 @@ pub struct ConfidentialTransferParams { /// A memo to include in the output, if any. This memo is encrypted in the output and can only be decrypted by the /// recipient pub memo: Option, + /// The last epoch the built transaction may be sequenced in. Mandatory: every transaction + /// carries a bounded validity window, so the caller decides how long this one stays + /// submittable. + pub max_epoch: Epoch, /// Run as a dry run, no funds will be transferred if true pub is_dry_run: bool, } diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/api.rs b/crates/wallet/sdk/src/apis/stealth_transfer/api.rs index 129c4a0005..b76dcceb85 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer/api.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/api.rs @@ -913,7 +913,7 @@ impl<'a, TSpec: WalletSdkSpec> StealthTransferApi<'a, TSpec> { let revealed_input_amount = transfer_statement.inputs_statement.revealed_amount; let revealed_output_amount = transfer_statement.outputs_statement.revealed_output_amount; - let transaction = Transaction::builder(network.as_byte()) + let transaction = Transaction::builder(network.as_byte(), params.max_epoch) .with_dry_run(params.is_dry_run) .with_fee_instructions_builder(|builder| { let fee_resource = params.fee_params.pay_fee_with_swap.as_ref().map(|swap| swap.input_resource).unwrap_or(TARI_TOKEN); @@ -1069,7 +1069,7 @@ impl<'a, TSpec: WalletSdkSpec> StealthTransferApi<'a, TSpec> { let revealed_to_recipients = params.total_revealed_output_amount(); let account_address = *owner_account.component_address(); - let transaction = Transaction::builder(network.as_byte()) + let transaction = Transaction::builder(network.as_byte(), params.max_epoch) .with_dry_run(params.is_dry_run) .with_fee_instructions_builder(|builder| { builder diff --git a/crates/wallet/sdk/src/apis/stealth_transfer/params.rs b/crates/wallet/sdk/src/apis/stealth_transfer/params.rs index 4996caf5d0..27b73c38b0 100644 --- a/crates/wallet/sdk/src/apis/stealth_transfer/params.rs +++ b/crates/wallet/sdk/src/apis/stealth_transfer/params.rs @@ -6,6 +6,7 @@ use ootle_network::Network; use tari_bor::{Deserialize, Serialize}; use tari_engine_types::crypto::MAX_LAZY_BP_AGG_FACTORS; use tari_ootle_address::OotleAddress; +use tari_ootle_common_types::Epoch; use tari_ootle_wallet_crypto::{memo::Memo, pay_to::PayTo, stealth::validated_condition_root}; use tari_template_lib::types::{Amount, ComponentAddress, NonFungibleAddress, ResourceAddress}; @@ -27,6 +28,10 @@ pub struct StealthTransferParams { pub resource_address: ResourceAddress, /// Fee to lock for the transaction pub max_fee: u64, + /// The last epoch the built transaction may be sequenced in. Mandatory: every transaction + /// carries a bounded validity window, so the caller decides how long this one stays + /// submittable. + pub max_epoch: Epoch, /// Run as a dry run, no funds will be transferred if true pub is_dry_run: bool, } @@ -257,6 +262,7 @@ mod tests { /// A single output of `blinded_amount` blinded plus `revealed_amount` revealed, paying to `pay_to`. fn params_paying_to(blinded_amount: u64, revealed_amount: u64, pay_to: PayTo) -> StealthTransferParams { StealthTransferParams { + max_epoch: Epoch(1), fee_params: TransferFeeParams::new(UtxoInputSelection::PreferConfidential), input_selection: UtxoInputSelection::PreferConfidential, outputs: vec![TransferOutput { diff --git a/crates/wallet/sdk/src/models/transaction_request.rs b/crates/wallet/sdk/src/models/transaction_request.rs index b159db180c..96f47fdbfb 100644 --- a/crates/wallet/sdk/src/models/transaction_request.rs +++ b/crates/wallet/sdk/src/models/transaction_request.rs @@ -155,7 +155,7 @@ mod tests { fn request(status: TransactionRequestStatus) -> TransactionRequestModel { TransactionRequestModel { id: 1, - unsigned_transaction: UnsignedTransaction::new(0u8), + unsigned_transaction: UnsignedTransaction::new(0u8, tari_ootle_common_types::Epoch(1)), seal_signer: KeyId::Derived { key_branch: KeyBranch::Account, index: 0u64, diff --git a/crates/wallet/sdk_tests/tests/balance_changes.rs b/crates/wallet/sdk_tests/tests/balance_changes.rs index c2f0a59482..50f3668d21 100644 --- a/crates/wallet/sdk_tests/tests/balance_changes.rs +++ b/crates/wallet/sdk_tests/tests/balance_changes.rs @@ -27,7 +27,7 @@ use tari_template_lib::types::{ use crate::support::Test; fn build_transaction() -> Transaction { - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("component") .put_last_instruction_output_on_workspace("bucket") .call_method("component", "new", args!["bucket"]) diff --git a/crates/wallet/storage_sqlite/tests/balance_changes.rs b/crates/wallet/storage_sqlite/tests/balance_changes.rs index 0b82470a75..9f48dd52c1 100644 --- a/crates/wallet/storage_sqlite/tests/balance_changes.rs +++ b/crates/wallet/storage_sqlite/tests/balance_changes.rs @@ -54,7 +54,7 @@ use tari_template_lib_types::{ }; fn build_transaction(seed: u64) -> Transaction { - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("component") .put_last_instruction_output_on_workspace("bucket") .call_method("component", "new", args!["bucket"]) diff --git a/crates/wallet/storage_sqlite/tests/transaction.rs b/crates/wallet/storage_sqlite/tests/transaction.rs index f3301c0367..d0337cc6ab 100644 --- a/crates/wallet/storage_sqlite/tests/transaction.rs +++ b/crates/wallet/storage_sqlite/tests/transaction.rs @@ -19,7 +19,7 @@ use tari_template_lib_types::ComponentAddress; fn build_transaction() -> Transaction { let key = RistrettoSecretKey::from(123); - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("component") .put_last_instruction_output_on_workspace("bucket") .call_method("component", "new", args!["bucket"]) @@ -31,7 +31,7 @@ fn build_transaction() -> Transaction { /// transaction id, so multiple transactions can be inserted in one test. fn build_linked_transaction(seed: u64) -> Transaction { let key = RistrettoSecretKey::from(seed); - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .allocate_component_address("component") .put_last_instruction_output_on_workspace("bucket") .call_method("component", "new", args!["bucket"]) diff --git a/crates/wallet/storage_sqlite/tests/transaction_requests.rs b/crates/wallet/storage_sqlite/tests/transaction_requests.rs index 7f6bf6c825..b670f9f0cc 100644 --- a/crates/wallet/storage_sqlite/tests/transaction_requests.rs +++ b/crates/wallet/storage_sqlite/tests/transaction_requests.rs @@ -14,7 +14,7 @@ use std::time::Duration; -use tari_ootle_common_types::optional::IsNotFoundError; +use tari_ootle_common_types::{Epoch, optional::IsNotFoundError}; use tari_ootle_transaction::{TransactionId, UnsignedTransaction}; use tari_ootle_wallet_sdk::{ models::{KeyBranch, KeyId, TransactionRequestId, TransactionRequestStatus}, @@ -44,7 +44,7 @@ fn insert_request(db: &SqliteWalletStore) -> TransactionRequestId { let mut tx = db.create_write_tx().unwrap(); let model = tx .transaction_request_insert( - &UnsignedTransaction::new(0u8), + &UnsignedTransaction::new(0u8, Epoch(1)), seal_signer(), &[], &[], @@ -347,7 +347,7 @@ fn a_ttl_past_the_representable_expiry_is_an_error() { let err = tx .transaction_request_insert( - &UnsignedTransaction::new(0u8), + &UnsignedTransaction::new(0u8, Epoch(1)), seal_signer(), &[], &[], diff --git a/integration_tests/src/util.rs b/integration_tests/src/util.rs index 2217eef794..e067751eba 100644 --- a/integration_tests/src/util.rs +++ b/integration_tests/src/util.rs @@ -2,16 +2,22 @@ // SPDX-License-Identifier: BSD-3-Clause use tari_ootle_address::Network; -use tari_ootle_transaction::TransactionBuilder; +use tari_ootle_transaction::{Epoch, TransactionBuilder}; /// The builder is stamped with a random nonce so that repeated identical calls (concurrent or /// sequential steps invoking the same method with unversioned inputs) build distinct /// transactions — the transaction id excludes the seal signature, so identical bodies sealed by /// the same key would otherwise be a single transaction. pub fn transaction_builder() -> TransactionBuilder { - TransactionBuilder::new(Network::LocalNet).with_nonce(rand::random()) + TransactionBuilder::new(Network::LocalNet, DEFAULT_TEST_MAX_EPOCH).with_nonce(rand::random()) } +/// Validity window for transactions built by the test harness. A cucumber network starts at a low +/// epoch and a scenario is short, so this outlives every run while staying well inside the +/// network's `max_transaction_validity_epochs` ceiling. Scenarios that pin a specific window +/// override it with `with_max_epoch`. +pub const DEFAULT_TEST_MAX_EPOCH: Epoch = Epoch(100); + #[macro_export] macro_rules! cucumber_log { ($($msg:tt)*) => {{ diff --git a/integration_tests/src/validator_node_client.rs b/integration_tests/src/validator_node_client.rs index 4310bde89d..cdcd326262 100644 --- a/integration_tests/src/validator_node_client.rs +++ b/integration_tests/src/validator_node_client.rs @@ -25,7 +25,13 @@ use tari_validator_node_client::{ }; use tokio::time::{MissedTickBehavior, interval}; -use crate::{TariWorld, cucumber_log, helpers::get_component_from_namespace, template::RegisteredTemplate}; +use crate::{ + TariWorld, + cucumber_log, + helpers::get_component_from_namespace, + template::RegisteredTemplate, + util::DEFAULT_TEST_MAX_EPOCH, +}; /// Creates a component by calling a template function pub async fn create_component( @@ -51,7 +57,7 @@ pub async fn create_component( let secret_key = get_signing_key(world).await; // Build and sign the transaction (Network::LocalNet == 0u8) - let transaction = TransactionBuilder::new(Network::LocalNet) + let transaction = TransactionBuilder::new(Network::LocalNet, DEFAULT_TEST_MAX_EPOCH) .call_function(template_address, function_call, parsed_args) .with_inputs(vec![]) .build_and_seal(&secret_key); @@ -291,7 +297,7 @@ async fn call_method_inner( })?; // Build transaction - let transaction = TransactionBuilder::new(Network::LocalNet) + let transaction = TransactionBuilder::new(Network::LocalNet, DEFAULT_TEST_MAX_EPOCH) .call_method(component_address, method_call, vec![]) .with_inputs(vec![component]) .build_and_seal(&secret_key); diff --git a/integration_tests/src/wallet_daemon_client.rs b/integration_tests/src/wallet_daemon_client.rs index 6f7b49737c..cd10bd1f59 100644 --- a/integration_tests/src/wallet_daemon_client.rs +++ b/integration_tests/src/wallet_daemon_client.rs @@ -449,7 +449,10 @@ pub async fn submit_manifest_with_signing_keys( .pay_fee_from_component(account.component_address, 5000u64) .with_instructions(instructions.instructions) .with_min_epoch(min_epoch) - .with_max_epoch(max_epoch) + .then(|b| match max_epoch { + Some(max_epoch) => b.with_max_epoch(max_epoch), + None => b, + }) .with_inputs(inputs.into_iter().map(|i| i.into_unversioned())) .build_unsigned(); @@ -538,7 +541,10 @@ pub async fn submit_manifest( .pay_fee_from_component(account.component_address, 5000u64) .with_instructions(instructions.instructions) .with_min_epoch(min_epoch) - .with_max_epoch(max_epoch) + .then(|b| match max_epoch { + Some(max_epoch) => b.with_max_epoch(max_epoch), + None => b, + }) .with_inputs(inputs) .build_unsigned(); @@ -655,7 +661,10 @@ pub async fn create_component( .pay_fee_from_component(account.component_address, 5000u64) .call_function(template_address, function_call, args) .with_min_epoch(min_epoch) - .with_max_epoch(max_epoch) + .then(|b| match max_epoch { + Some(max_epoch) => b.with_max_epoch(max_epoch), + None => b, + }) .build_unsigned(); let transaction_submit_req = TransactionSubmitRequest { diff --git a/utilities/tariswap_test_bench/src/runner.rs b/utilities/tariswap_test_bench/src/runner.rs index 9a654b4fd8..1842d86bd4 100644 --- a/utilities/tariswap_test_bench/src/runner.rs +++ b/utilities/tariswap_test_bench/src/runner.rs @@ -6,6 +6,7 @@ use std::{path::Path, str::FromStr, time::Duration}; use log::info; use tari_crypto::tari_utilities::SafePassword; use tari_engine_types::commit_result::FinalizeResult; +use tari_ootle_common_types::Epoch; use tari_ootle_transaction::{Network, Transaction, TransactionBuilder, TransactionId}; use tari_ootle_wallet_sdk::{ WalletSdk as Sdk, @@ -13,6 +14,7 @@ use tari_ootle_wallet_sdk::{ cipher_seed::CipherSeedRestore, local_key_store::LocalKeyStore, models::EpochBirthday, + network::WalletNetworkInterface, }; use tari_ootle_wallet_sdk_services::indexer_rest_api::IndexerRestApiNetworkInterface; use tari_ootle_wallet_storage_sqlite::SqliteWalletStore; @@ -37,18 +39,27 @@ pub struct Runner { pub(crate) faucet_template: TemplateAddress, pub(crate) tariswap_template: TemplateAddress, pub(crate) stats: Stats, + /// Validity window shared by every transaction the bench builds, resolved once at startup. A + /// bench run is short relative to this, so a single window covers the whole run. + pub(crate) max_epoch: Epoch, } +/// Epochs past the epoch at startup that bench transactions stay valid for. +const BENCH_TRANSACTION_VALIDITY_EPOCHS: u64 = 10; + impl Runner { pub async fn init(cli: CommonArgs) -> anyhow::Result { let sdk = initialize_wallet_sdk(&cli.db_path, cli.indexer_url.clone())?; let (faucet_template, tariswap_template) = get_templates(&cli).await?; + let current_epoch = sdk.get_network_interface().get_current_epoch().await?; + let max_epoch = Epoch(current_epoch.as_u64() + BENCH_TRANSACTION_VALIDITY_EPOCHS); Ok(Self { sdk, cli, faucet_template, tariswap_template, stats: Stats::default(), + max_epoch, }) } @@ -117,7 +128,7 @@ impl Runner { } pub fn new_transaction_builder(&self) -> TransactionBuilder { - Transaction::builder(self.cli.network) + Transaction::builder(self.cli.network, self.max_epoch) } } diff --git a/utilities/traffic-sim/src/sim.rs b/utilities/traffic-sim/src/sim.rs index 31143a821c..272d7a64c2 100644 --- a/utilities/traffic-sim/src/sim.rs +++ b/utilities/traffic-sim/src/sim.rs @@ -21,7 +21,7 @@ use tari_ootle_common_types::{ engine_types::published_template::PublishedTemplateAddress, optional::Optional, }; -use tari_ootle_transaction::{Network, Transaction, args}; +use tari_ootle_transaction::{Epoch, Network, Transaction, args}; use tari_ootle_wallet_sdk::{ apis::{ confidential_transfer::UtxoInputSelection, @@ -68,7 +68,17 @@ pub struct Wallet { pub network: Network, } +/// Validity window stamped on simulated transactions. Well within any network's ceiling, and long +/// enough that a slow simulation run never has a transaction expire mid-flight. +const SIM_TRANSACTION_VALIDITY_EPOCHS: u64 = 10; + impl Wallet { + /// A validity window ending [`SIM_TRANSACTION_VALIDITY_EPOCHS`] past the daemon's current epoch. + pub async fn max_epoch(&mut self) -> anyhow::Result { + let settings = self.client.get_settings().await?; + Ok(Epoch(settings.current_epoch.as_u64() + SIM_TRANSACTION_VALIDITY_EPOCHS)) + } + pub async fn connect(name: String, address: &str) -> anyhow::Result { let mut client = WalletDaemonClient::connect(address, None)?; let resp = client @@ -304,7 +314,8 @@ impl TrafficSim { }, }; - let transaction = Transaction::builder(exchange_wallet.network.as_byte()) + let max_epoch = exchange_wallet.max_epoch().await?; + let transaction = Transaction::builder(exchange_wallet.network.as_byte(), max_epoch) .pay_fee_from_component(*account.component_address(), 3000u64) .allocate_component_address("sc") .call_function(stablecoin_template, "instantiate", args![ @@ -411,6 +422,8 @@ impl TrafficSim { .owner_key_id .ok_or_else(|| anyhow::anyhow!("Exchange account has no owner key ID"))?; + let max_epoch = exchange_wallet.max_epoch().await?; + for (wallet, account) in self.wallet_and_account_iter() { let mut client = wallet.client.clone(); @@ -482,7 +495,7 @@ impl TrafficSim { }) .await?; - let transaction = Transaction::builder(wallet.network.as_byte()) + let transaction = Transaction::builder(wallet.network.as_byte(), max_epoch) .pay_fee_from_component(*exchange_account.component_address(), 2000u64) .call_method(*exchange_account.component_address(), "create_proof_by_amount", args![ admin_resource_address, diff --git a/utilities/transaction_generator/src/cli.rs b/utilities/transaction_generator/src/cli.rs index 59f4ba6d8a..6d180078b6 100644 --- a/utilities/transaction_generator/src/cli.rs +++ b/utilities/transaction_generator/src/cli.rs @@ -67,6 +67,12 @@ pub struct WriteArgs { pub signer_secret_key: Option, #[clap(long, short = 't')] pub network: Option, + /// The last epoch the generated transactions are valid in. Every transaction carries a mandatory + /// validity window, so this must be set to an epoch the target network will still accept when the + /// generated file is submitted: no earlier than the current epoch and no more than + /// `max_transaction_validity_epochs` beyond it. + #[clap(long)] + pub max_epoch: u64, } #[derive(Args, Debug)] pub struct ReadArgs { diff --git a/utilities/transaction_generator/src/main.rs b/utilities/transaction_generator/src/main.rs index f951d4e63c..97d23254ce 100644 --- a/utilities/transaction_generator/src/main.rs +++ b/utilities/transaction_generator/src/main.rs @@ -14,7 +14,7 @@ use std::{ use anyhow::anyhow; use cli::Cli; use tari_crypto::{keys::SecretKey, ristretto::RistrettoSecretKey, tari_utilities::hex::Hex}; -use tari_ootle_common_types::SubstateRequirement; +use tari_ootle_common_types::{Epoch, SubstateRequirement}; use tari_ootle_transaction::{Blob, Network}; use tari_template_lib_types::TemplateAddress; use tari_transaction_manifest::ManifestValue; @@ -121,9 +121,10 @@ fn get_transaction_builder(args: &WriteArgs) -> anyhow::Result Ok(Box::new(free_coins::builder(network))), + None => Ok(Box::new(free_coins::builder(network, Epoch(args.max_epoch)))), } } diff --git a/utilities/transaction_generator/src/transaction_builders/free_coins.rs b/utilities/transaction_generator/src/transaction_builders/free_coins.rs index dbfacb83f0..f05a83add3 100644 --- a/utilities/transaction_generator/src/transaction_builders/free_coins.rs +++ b/utilities/transaction_generator/src/transaction_builders/free_coins.rs @@ -3,7 +3,7 @@ use ootle_byte_type::ToByteType; use tari_crypto::{keys::PublicKey, ristretto::RistrettoPublicKey}; -use tari_ootle_common_types::SubstateRequirement; +use tari_ootle_common_types::{Epoch, SubstateRequirement}; use tari_ootle_transaction::{Network, Transaction, args}; use tari_template_lib_types::constants::{ TARI_TOKEN, @@ -12,12 +12,12 @@ use tari_template_lib_types::constants::{ XTR_FAUCET_VAULT_ADDRESS, }; -pub fn builder(network: Network) -> impl Fn(u64) -> Transaction { +pub fn builder(network: Network, max_epoch: Epoch) -> impl Fn(u64) -> Transaction { move |_: u64| -> Transaction { let (signer_secret_key, signer_public_key) = RistrettoPublicKey::random_keypair(&mut rand::rng()); let signer_public_key = signer_public_key.to_byte_type(); - Transaction::builder(network.as_byte()) + Transaction::builder(network.as_byte(), max_epoch) .with_fee_instructions_builder(|builder| { builder .create_account(signer_public_key) diff --git a/utilities/transaction_generator/src/transaction_builders/manifest.rs b/utilities/transaction_generator/src/transaction_builders/manifest.rs index 845b651f2c..d9a3b12134 100644 --- a/utilities/transaction_generator/src/transaction_builders/manifest.rs +++ b/utilities/transaction_generator/src/transaction_builders/manifest.rs @@ -8,7 +8,7 @@ use tari_crypto::{ keys::PublicKey, ristretto::{RistrettoPublicKey, RistrettoSecretKey}, }; -use tari_ootle_common_types::SubstateRequirement; +use tari_ootle_common_types::{Epoch, SubstateRequirement}; use tari_ootle_transaction::{Blob, Network, Transaction}; use tari_template_lib_types::TemplateAddress; use tari_transaction_manifest::ManifestValue; @@ -25,6 +25,7 @@ pub fn builder>( extra_inputs: Vec, blob_inputs: HashMap, random_signer: bool, + max_epoch: Epoch, ) -> anyhow::Result { let contents = fs::read_to_string(manifest)?; // Every substate referenced by the transaction must be declared as an input, otherwise the @@ -47,7 +48,7 @@ pub fn builder>( let blobs = instructions.blobs; Ok(Box::new(move |_| { - let mut builder = Transaction::builder(network.as_byte()) + let mut builder = Transaction::builder(network.as_byte(), max_epoch) .with_fee_instructions(fee_instructions.clone()) .with_instructions(main_instructions.clone()); for (i, blob) in blobs.iter().enumerate() { diff --git a/utilities/transaction_generator/tests/manifest_inputs.rs b/utilities/transaction_generator/tests/manifest_inputs.rs index 5964a9cf54..3f88d055dd 100644 --- a/utilities/transaction_generator/tests/manifest_inputs.rs +++ b/utilities/transaction_generator/tests/manifest_inputs.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use tari_crypto::ristretto::RistrettoSecretKey; use tari_ootle_common_types::SubstateRequirement; -use tari_ootle_transaction::Network; +use tari_ootle_transaction::{Epoch, Network}; use tari_template_lib_types::TemplateAddress; use tari_transaction_manifest::ManifestValue; use transaction_generator::transaction_builders::manifest; @@ -36,6 +36,7 @@ fn declares_account_arg_and_explicit_input_as_transaction_inputs() { extra_inputs, HashMap::new(), false, + Epoch(1), ) .unwrap(); diff --git a/utilities/transaction_generator/tests/max_compute.rs b/utilities/transaction_generator/tests/max_compute.rs index 31642586d5..05e83acd1e 100644 --- a/utilities/transaction_generator/tests/max_compute.rs +++ b/utilities/transaction_generator/tests/max_compute.rs @@ -7,7 +7,7 @@ //! retuned if the cost drifts. use tari_engine_types::{commit_result::RejectReason, fees::FeeSource}; -use tari_ootle_transaction::{Transaction, args}; +use tari_ootle_transaction::{Epoch, Transaction, args}; use tari_template_test_tooling::TemplateTest; const CRATE_PATH: &str = env!("CARGO_MANIFEST_DIR"); @@ -34,7 +34,7 @@ fn busy_max_stays_under_the_metering_budget() { // Succeeds => the single busy_max() call did not exhaust its metering budget. let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, 900_000_000u64) .call_function(addr, "busy_max", args![]) .build_and_seal(&key), @@ -72,7 +72,7 @@ fn busy_cost_is_linear_in_rounds() { let mut busy = |rounds: u64| { let result = test.execute_expect_success( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .pay_fee_from_component(account, 900_000_000u64) .call_function(addr, "busy", args![rounds]) .build_and_seal(&key), @@ -112,7 +112,7 @@ fn stacked_busy_max_is_rejected_by_per_transaction_budget() { // Fees are disabled here on purpose: the per-transaction budget is enforced independently of fee // charging, so the rejection must be the out-of-gas execution failure, not insufficient fees. let reason = test.execute_expect_failure( - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(1)) .call_function(addr, "busy_max", args![]) .call_function(addr, "busy_max", args![]) .build_and_seal(test.secret_key()), diff --git a/utilities/transaction_generator/tests/publish_template.rs b/utilities/transaction_generator/tests/publish_template.rs index 2b10c171ca..46e9233f55 100644 --- a/utilities/transaction_generator/tests/publish_template.rs +++ b/utilities/transaction_generator/tests/publish_template.rs @@ -11,7 +11,7 @@ use tari_crypto::{ keys::{PublicKey, SecretKey}, ristretto::{RistrettoPublicKey, RistrettoSecretKey}, }; -use tari_ootle_transaction::{Blob, Instruction, Network}; +use tari_ootle_transaction::{Blob, Epoch, Instruction, Network}; use tari_transaction_manifest::ManifestValue; use transaction_generator::transaction_builders::manifest; @@ -46,6 +46,7 @@ fn random_signer_makes_each_publish_address_distinct() { Vec::new(), blob_inputs(), true, + Epoch(1), ) .unwrap(); @@ -113,6 +114,7 @@ fn without_random_signer_a_single_signer_seals() { Vec::new(), blob_inputs(), false, + Epoch(1), ) .unwrap(); diff --git a/utilities/transaction_generator/tests/publish_template_engine.rs b/utilities/transaction_generator/tests/publish_template_engine.rs index 9cb6f0b46b..f25dfaf5bf 100644 --- a/utilities/transaction_generator/tests/publish_template_engine.rs +++ b/utilities/transaction_generator/tests/publish_template_engine.rs @@ -17,7 +17,7 @@ use tari_engine_types::{ commit_result::ExecuteResult, substate::{SubstateId, SubstateValue}, }; -use tari_ootle_transaction::{Blob, Network}; +use tari_ootle_transaction::{Blob, Epoch, Network}; use tari_template_test_tooling::{TemplateTest, compile::compile_template}; use tari_transaction_manifest::ManifestValue; use transaction_generator::transaction_builders::manifest; @@ -55,6 +55,7 @@ fn random_signer_publishes_same_binary_twice_without_duplicating() { Vec::new(), blob_inputs, true, + Epoch(1), ) .unwrap(); From 02287540123616ee172f6837bb844380653e759d Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Thu, 13 Aug 2026 13:17:53 +0400 Subject: [PATCH 2/8] fix(ootle-rs): set max_epoch at feature-gated and doc builder sites The ledger-feature signer test, the ledger client's preimage fixture and the crate-level doc example construct transactions on paths that a default-feature build never compiles. Co-Authored-By: Claude Opus 5 --- crates/wallet/ledger/client/src/client.rs | 3 ++- crates/wallet/ootle-rs/src/lib.rs | 13 +++++++++---- crates/wallet/ootle-rs/src/signer/ledger.rs | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/wallet/ledger/client/src/client.rs b/crates/wallet/ledger/client/src/client.rs index 9c78c91bf9..60a023e3ea 100644 --- a/crates/wallet/ledger/client/src/client.rs +++ b/crates/wallet/ledger/client/src/client.rs @@ -235,6 +235,7 @@ mod tests { use tari_ootle_transaction::{ Blob, Blobs, + Epoch, Instruction, PreimageSegment, TransactionSealSignature, @@ -256,7 +257,7 @@ mod tests { ], inputs: IndexSet::new(), min_epoch: None, - max_epoch: None, + max_epoch: Epoch(1), is_seal_signer_authorized: false, dry_run: false, nonce: 0, diff --git a/crates/wallet/ootle-rs/src/lib.rs b/crates/wallet/ootle-rs/src/lib.rs index 2ffc0f4bf9..7c50ecb63a 100644 --- a/crates/wallet/ootle-rs/src/lib.rs +++ b/crates/wallet/ootle-rs/src/lib.rs @@ -22,7 +22,7 @@ //! key_provider::PrivateKeyProvider, //! provider::ProviderBuilder, //! wallet::OotleWallet, -//! Network, TransactionRequest, +//! Epoch, Network, TransactionRequest, //! }; //! use tari_template_lib_types::constants::TARI_TOKEN; //! @@ -39,8 +39,12 @@ //! .connect("http://127.0.0.1:12500") //! .await?; //! +//! // Every transaction declares the last epoch it may be sequenced in; past it the +//! // transaction can never land. The network caps how far ahead this may be set. +//! let max_epoch = Epoch(provider.get_epoch().await?.as_u64() + 10); +//! //! // 3. Fund our account from the faucet -//! let faucet_tx = IFaucet::new(&provider) +//! let faucet_tx = IFaucet::new(&provider, max_epoch) //! .pay_fee(1000u64) //! .take_free_coins(500_000_000u64) //! .prepare() @@ -55,7 +59,7 @@ //! // 4. Transfer tokens to a recipient //! let recipient = address!("otl_loc_10mc0v2lyy43kldl0ft4c2x5pe7j0ckduv8zej6jgr2z2g9m07fz7gl96ar5wwgu0qu0atmr5tl53ye7n38xr5u7ytlmudq0ruxcau0gge7rxk"); //! -//! let unsigned_tx = IAccount::new(&provider) +//! let unsigned_tx = IAccount::new(&provider, max_epoch) //! .pay_fee(1000u64) //! .public_transfer(&recipient, TARI_TOKEN, 1_000_000u64) //! .prepare() @@ -157,7 +161,8 @@ mod types; // Re-export the address macro from the ootle_address crate pub use helpers::*; pub use tari_ootle_address::{Network, address}; -pub use tari_ootle_common_types::displayable; +// Re-exported because every transaction builder takes a `max_epoch`, so callers need the type. +pub use tari_ootle_common_types::{Epoch, displayable}; pub use tari_ootle_wallet_crypto as crypto; pub use tari_template_lib_types as template_types; pub use types::*; diff --git a/crates/wallet/ootle-rs/src/signer/ledger.rs b/crates/wallet/ootle-rs/src/signer/ledger.rs index 5c7b787d74..6b6559c6cd 100644 --- a/crates/wallet/ootle-rs/src/signer/ledger.rs +++ b/crates/wallet/ootle-rs/src/signer/ledger.rs @@ -264,7 +264,7 @@ mod tests { keys::{PublicKey, SecretKey}, ristretto::RistrettoSecretKey, }; - use tari_ootle_transaction::{Transaction as TransactionBuilderEntry, UnsealedTransactionV1}; + use tari_ootle_transaction::{Epoch, Transaction as TransactionBuilderEntry, UnsealedTransactionV1}; use super::*; @@ -302,7 +302,7 @@ mod tests { #[tokio::test] async fn signing_a_transaction_from_another_network_is_rejected() { let signer = signer_on(Network::Igor); - let tx = TransactionBuilderEntry::builder(Network::LocalNet).build_unsigned(); + let tx = TransactionBuilderEntry::builder(Network::LocalNet, Epoch(1)).build_unsigned(); let nonce = RistrettoPublicKey::from_secret_key(&RistrettoSecretKey::random(&mut rand::rng())); let err = signer From eae587230748ca2bfd9458b150d5c20907bace98 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Thu, 13 Aug 2026 13:55:59 +0400 Subject: [PATCH 3/8] feat(web-ui): show max epoch on transaction views and add an indexer liveness pill The transaction detail views in the wallet, indexer and validator node UIs now show the validity window alongside the other transaction facts, so the epoch a transaction dies in is visible where it is inspected. The wallet gains a liveness pill in the app bar, probing the configured indexer on the same endpoint the indexer settings tab uses and reading the URL from settings so a change takes effect without a reload. One failed probe reads as the intermediate state rather than an outage. settings.get no longer fails when the indexer is unreachable: it reports a null current_epoch instead. Settings is where the indexer URL is corrected, so it has to stay readable while the network is down. Callers that need the epoch to pick a validity window (wallet CLI, traffic-sim) now say so explicitly rather than guessing. Co-Authored-By: Claude Opus 5 --- .../routes/Transaction/components/Result.tsx | 38 +++-- .../Transactions/TransactionDetails.tsx | 10 ++ .../src/command/transaction.rs | 21 ++- .../tari_walletd/src/handlers/settings.rs | 4 +- .../src/components/IndexerLivenessPill.tsx | 151 ++++++++++++++++++ .../Transactions/TransactionDetails.tsx | 12 ++ .../web_ui/src/theme/LayoutMain.tsx | 2 + bindings/src/types/EncodedMerkleProof.ts | 6 +- .../GetNonFungiblesRequest.ts | 6 +- bindings/src/types/wallet-types/KeyId.ts | 4 +- .../types/wallet-types/SettingsGetResponse.ts | 2 +- clients/wallet_daemon_client/src/types.rs | 8 +- utilities/traffic-sim/src/sim.rs | 5 +- 13 files changed, 234 insertions(+), 35 deletions(-) create mode 100644 applications/tari_walletd/web_ui/src/components/IndexerLivenessPill.tsx diff --git a/applications/tari_indexer/web_ui/src/routes/Transaction/components/Result.tsx b/applications/tari_indexer/web_ui/src/routes/Transaction/components/Result.tsx index 00dbe7dd26..d332d2983c 100644 --- a/applications/tari_indexer/web_ui/src/routes/Transaction/components/Result.tsx +++ b/applications/tari_indexer/web_ui/src/routes/Transaction/components/Result.tsx @@ -44,11 +44,7 @@ import { useQueryClient } from "@tanstack/react-query"; import { saveAs } from "file-saver"; import { useState } from "react"; import { useGetTransaction, useGetTransactionResult } from "../../../api/hooks/useTransactions"; -import { - Accordion, - AccordionDetails, - AccordionSummary, -} from "../../../Components/Accordion"; +import { Accordion, AccordionDetails, AccordionSummary } from "../../../Components/Accordion"; import FetchStatusCheck from "../../../Components/FetchStatusCheck"; import StatusChip from "../../../Components/StatusChip"; import { DataTableCell } from "../../../Components/StyledComponents"; @@ -122,20 +118,15 @@ function Result({ transaction_id }: IndexerGetTransactionResultRequest) { } const execResult: any = - data?.result && isFinalized(data.result) - ? data.result.Finalized.execution_result?.finalize?.result - : undefined; + data?.result && isFinalized(data.result) ? data.result.Finalized.execution_result?.finalize?.result : undefined; const rejected = data?.result && isRejected(data.result) ? data.result.Rejected : undefined; const handleChange = (panel: string) => (_event: React.SyntheticEvent, isExpanded: boolean) => { - setExpandedPanels((prev) => - isExpanded ? [...prev, panel] : prev.filter((p) => p !== panel), - ); + setExpandedPanels((prev) => (isExpanded ? [...prev, panel] : prev.filter((p) => p !== panel))); }; - const expandAll = () => - setExpandedPanels(["p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8", "p9", "p10"]); + const expandAll = () => setExpandedPanels(["p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8", "p9", "p10"]); const collapseAll = () => setExpandedPanels([]); @@ -205,6 +196,18 @@ function Result({ transaction_id }: IndexerGetTransactionResultRequest) { {data.result.Finalized.abort_details} )} + {transaction?.min_epoch != null && ( + + Min Epoch + {transaction.min_epoch.toString()} + + )} + {transaction?.max_epoch != null && ( + + Max Epoch + {transaction.max_epoch.toString()} + + )} Download @@ -278,15 +281,10 @@ function Result({ transaction_id }: IndexerGetTransactionResultRequest) { {/* Blobs */} - - Blobs ({transaction?.blob_hashes?.length ?? 0}) - + Blobs ({transaction?.blob_hashes?.length ?? 0}) - + diff --git a/applications/tari_validator_node/web_ui/src/routes/Transactions/TransactionDetails.tsx b/applications/tari_validator_node/web_ui/src/routes/Transactions/TransactionDetails.tsx index 97b601d03e..d1da32acd5 100644 --- a/applications/tari_validator_node/web_ui/src/routes/Transactions/TransactionDetails.tsx +++ b/applications/tari_validator_node/web_ui/src/routes/Transactions/TransactionDetails.tsx @@ -190,6 +190,16 @@ export default function TransactionDetails() { Total Fees {fee?.toString()} + {transaction?.min_epoch != null && ( + + Min Epoch + {transaction.min_epoch.toString()} + + )} + + Max Epoch + {transaction?.max_epoch?.toString()} + {final_decision && ( Status diff --git a/applications/tari_wallet_cli/src/command/transaction.rs b/applications/tari_wallet_cli/src/command/transaction.rs index 2e2c9f2486..143e0b13d4 100644 --- a/applications/tari_wallet_cli/src/command/transaction.rs +++ b/applications/tari_wallet_cli/src/command/transaction.rs @@ -133,10 +133,19 @@ pub struct CommonSubmitArgs { impl CommonSubmitArgs { /// The caller's explicit `max_epoch`, or the daemon's default window past the current epoch. - fn resolve_max_epoch(&self, current_epoch: Epoch, default_validity_epochs: u64) -> Epoch { - self.max_epoch - .map(Epoch) - .unwrap_or_else(|| Epoch(current_epoch.as_u64().saturating_add(default_validity_epochs))) + /// Without either — the daemon could not reach its indexer and no `--max-epoch` was given — + /// there is no window the network is known to accept, so fail rather than guess. + fn resolve_max_epoch(&self, current_epoch: Option, default_validity_epochs: u64) -> anyhow::Result { + if let Some(max_epoch) = self.max_epoch { + return Ok(Epoch(max_epoch)); + } + let current_epoch = current_epoch.ok_or_else(|| { + anyhow!( + "The wallet daemon could not reach its indexer, so the current epoch is unknown. Pass --max-epoch to \ + choose the transaction's validity window explicitly." + ) + })?; + Ok(Epoch(current_epoch.as_u64().saturating_add(default_validity_epochs))) } } @@ -273,7 +282,7 @@ pub async fn handle_submit(args: SubmitArgs, client: &mut WalletDaemonClient) -> default_transaction_validity_epochs, .. } = client.get_settings().await?; - let max_epoch = common.resolve_max_epoch(current_epoch, default_transaction_validity_epochs); + let max_epoch = common.resolve_max_epoch(current_epoch, default_transaction_validity_epochs)?; let mut builder = Transaction::builder(network.byte, max_epoch) .call_method(fee_account.component_address, "withdraw", args![common.max_fee,]) @@ -358,7 +367,7 @@ async fn handle_submit_manifest( default_transaction_validity_epochs, .. } = client.get_settings().await?; - let max_epoch = common.resolve_max_epoch(current_epoch, default_transaction_validity_epochs); + let max_epoch = common.resolve_max_epoch(current_epoch, default_transaction_validity_epochs)?; let builder = Transaction::builder(network.byte, max_epoch) .with_fee_instructions_builder(|builder| { diff --git a/applications/tari_walletd/src/handlers/settings.rs b/applications/tari_walletd/src/handlers/settings.rs index c80fc184b5..782352d56b 100644 --- a/applications/tari_walletd/src/handlers/settings.rs +++ b/applications/tari_walletd/src/handlers/settings.rs @@ -35,7 +35,9 @@ pub async fn handle_get( .optional()? .unwrap_or_default(); - let current_epoch = context.current_epoch().await?; + // Deliberately not fatal: the indexer being down must not make settings unreadable, since this + // is where the indexer URL is corrected. + let current_epoch = context.current_epoch().await.ok(); Ok(SettingsGetResponse { indexer_url, diff --git a/applications/tari_walletd/web_ui/src/components/IndexerLivenessPill.tsx b/applications/tari_walletd/web_ui/src/components/IndexerLivenessPill.tsx new file mode 100644 index 0000000000..a49b1ebc3f --- /dev/null +++ b/applications/tari_walletd/web_ui/src/components/IndexerLivenessPill.tsx @@ -0,0 +1,151 @@ +// Copyright 2026. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import Box from "@mui/material/Box"; +import Chip from "@mui/material/Chip"; +import Tooltip from "@mui/material/Tooltip"; +import { useTheme } from "@mui/material/styles"; +import { indexerGetNetworkInfo, settingsGet } from "@utils/json_rpc"; +import { useEffect, useRef, useState } from "react"; + +/// How often the configured indexer is re-probed. +const POLL_INTERVAL_MS = 15_000; + +/// Consecutive failures tolerated before the pill goes red. One failed probe is reported as the +/// intermediate state so a single dropped request does not read as an outage. +const FAILURES_BEFORE_DISCONNECTED = 2; + +type Liveness = "connected" | "degraded" | "disconnected"; + +interface Status { + liveness: Liveness; + epoch: bigint | number | null; + indexerUrl: string | null; + detail: string | null; +} + +const LABELS: Record = { + connected: "Connected", + degraded: "Connecting…", + disconnected: "Disconnected", +}; + +/** + * Liveness of the indexer this wallet is configured to use, as a coloured pill. + * + * Probes the same endpoint the indexer settings tab uses, reading the URL from `settings.get` so + * that changing the indexer takes effect here without a reload. The probe goes browser → indexer + * directly, so it reflects whether *this* page can reach the indexer. + */ +function IndexerLivenessPill() { + const theme = useTheme(); + const [status, setStatus] = useState({ + liveness: "degraded", + epoch: null, + indexerUrl: null, + detail: null, + }); + // Counted across polls rather than held in state, so a re-probe does not re-render the pill + // before it has a verdict. + const consecutiveFailures = useRef(0); + + useEffect(() => { + let cancelled = false; + + const probe = async () => { + try { + const settings = await settingsGet(); + const indexerUrl = settings.indexer_url; + if (!indexerUrl) { + throw new Error("No indexer URL is configured"); + } + const info = await indexerGetNetworkInfo(indexerUrl); + if (cancelled) return; + consecutiveFailures.current = 0; + setStatus({ + liveness: "connected", + epoch: info.epoch, + indexerUrl, + detail: null, + }); + } catch (e) { + if (cancelled) return; + consecutiveFailures.current += 1; + const detail = e instanceof Error ? e.message : "Cannot reach the indexer"; + setStatus((prev) => ({ + ...prev, + liveness: consecutiveFailures.current >= FAILURES_BEFORE_DISCONNECTED ? "disconnected" : "degraded", + detail, + })); + } + }; + + void probe(); + const timer = setInterval(() => void probe(), POLL_INTERVAL_MS); + return () => { + cancelled = true; + clearInterval(timer); + }; + }, []); + + const colour = { + connected: theme.palette.success.main, + degraded: theme.palette.warning.main, + disconnected: theme.palette.error.main, + }[status.liveness]; + + const tooltip = [ + status.indexerUrl ? `Indexer: ${status.indexerUrl}` : "No indexer configured", + status.liveness === "connected" && status.epoch != null ? `Epoch: ${status.epoch.toString()}` : null, + status.detail, + ] + .filter(Boolean) + .join(" · "); + + return ( + + + } + sx={{ + borderColor: colour, + color: theme.palette.text.secondary, + }} + /> + + ); +} + +export default IndexerLivenessPill; diff --git a/applications/tari_walletd/web_ui/src/routes/Transactions/TransactionDetails.tsx b/applications/tari_walletd/web_ui/src/routes/Transactions/TransactionDetails.tsx index bc03098996..5322e396c9 100644 --- a/applications/tari_walletd/web_ui/src/routes/Transactions/TransactionDetails.tsx +++ b/applications/tari_walletd/web_ui/src/routes/Transactions/TransactionDetails.tsx @@ -205,6 +205,18 @@ export default function TransactionDetails() { {data.invalid_reason ? data.invalid_reason : renderResult(data?.result)} + {transaction?.min_epoch != null && ( + + Min Epoch + {transaction.min_epoch.toString()} + + )} + {transaction?.max_epoch != null && ( + + Max Epoch + {transaction.max_epoch.toString()} + + )} JSON diff --git a/applications/tari_walletd/web_ui/src/theme/LayoutMain.tsx b/applications/tari_walletd/web_ui/src/theme/LayoutMain.tsx index e59900fb1c..bf373ead98 100644 --- a/applications/tari_walletd/web_ui/src/theme/LayoutMain.tsx +++ b/applications/tari_walletd/web_ui/src/theme/LayoutMain.tsx @@ -25,6 +25,7 @@ import "@tari-project/ootle-web-ui-theming/overrides.ts"; import { breadcrumbRoutes } from "@/App"; import Logo from "@assets/Logo"; import Breadcrumbs from "@components/Breadcrumbs"; +import IndexerLivenessPill from "@components/IndexerLivenessPill"; import MenuItems from "@components/MenuItems"; import { Check } from "@mui/icons-material"; import MenuOpenOutlinedIcon from "@mui/icons-material/MenuOpenOutlined"; @@ -179,6 +180,7 @@ export default function Layout() { > + {/**/} {/* {loggedIn ? : null}*/} {/**/} diff --git a/bindings/src/types/EncodedMerkleProof.ts b/bindings/src/types/EncodedMerkleProof.ts index 9eebb01ee1..b59a91465f 100644 --- a/bindings/src/types/EncodedMerkleProof.ts +++ b/bindings/src/types/EncodedMerkleProof.ts @@ -1,3 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type EncodedMerkleProof = { block_hash: string; encoded_merkle_proof: string; leaf_index: number | bigint | string }; +export type EncodedMerkleProof = { + block_hash: string; + encoded_merkle_proof: string; + leaf_index: number | bigint | string; +}; diff --git a/bindings/src/types/tari-indexer-client/GetNonFungiblesRequest.ts b/bindings/src/types/tari-indexer-client/GetNonFungiblesRequest.ts index b5ea14c838..1d9d881cd1 100644 --- a/bindings/src/types/tari-indexer-client/GetNonFungiblesRequest.ts +++ b/bindings/src/types/tari-indexer-client/GetNonFungiblesRequest.ts @@ -1,4 +1,8 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { ResourceAddress } from "../ResourceAddress"; -export type GetNonFungiblesRequest = { address: ResourceAddress; start_index: number | bigint | string; end_index: number | bigint | string }; +export type GetNonFungiblesRequest = { + address: ResourceAddress; + start_index: number | bigint | string; + end_index: number | bigint | string; +}; diff --git a/bindings/src/types/wallet-types/KeyId.ts b/bindings/src/types/wallet-types/KeyId.ts index d08c2c5336..572ccd8591 100644 --- a/bindings/src/types/wallet-types/KeyId.ts +++ b/bindings/src/types/wallet-types/KeyId.ts @@ -1,4 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { KeyBranch } from "./KeyBranch"; -export type KeyId = { Derived: { key_branch: KeyBranch; index: number | bigint | string } } | { Imported: { local_key_id: number | bigint | string } }; +export type KeyId = + | { Derived: { key_branch: KeyBranch; index: number | bigint | string } } + | { Imported: { local_key_id: number | bigint | string } }; diff --git a/bindings/src/types/wallet-types/SettingsGetResponse.ts b/bindings/src/types/wallet-types/SettingsGetResponse.ts index 7351e0f568..a1ec6a107e 100644 --- a/bindings/src/types/wallet-types/SettingsGetResponse.ts +++ b/bindings/src/types/wallet-types/SettingsGetResponse.ts @@ -7,6 +7,6 @@ export type SettingsGetResponse = { network: NetworkInfo; advanced_ui_features: AdvancedUiFeatures; claimed_accounts: Array; - current_epoch: number; + current_epoch: number | null; default_transaction_validity_epochs: number; }; diff --git a/clients/wallet_daemon_client/src/types.rs b/clients/wallet_daemon_client/src/types.rs index c7fb5ae8e7..1ce4f6c2ea 100644 --- a/clients/wallet_daemon_client/src/types.rs +++ b/clients/wallet_daemon_client/src/types.rs @@ -1337,9 +1337,11 @@ pub struct SettingsGetResponse { pub advanced_ui_features: AdvancedUiFeatures, pub claimed_accounts: Vec, /// The network's current epoch, as the wallet daemon sees it. Callers that build transactions - /// themselves need it to choose a `max_epoch` the network will accept. - #[cfg_attr(feature = "ts", ts(type = "number"))] - pub current_epoch: Epoch, + /// themselves need it to choose a `max_epoch` the network will accept. `None` when the indexer + /// is unreachable — settings must stay readable while the network is down, not least so the + /// caller can see and correct the indexer URL. + #[cfg_attr(feature = "ts", ts(type = "number | null"))] + pub current_epoch: Option, /// How many epochs past `current_epoch` the wallet daemon stamps `max_epoch` when a caller does /// not choose one. #[cfg_attr(feature = "ts", ts(type = "number"))] diff --git a/utilities/traffic-sim/src/sim.rs b/utilities/traffic-sim/src/sim.rs index 272d7a64c2..d3e9f1e2b3 100644 --- a/utilities/traffic-sim/src/sim.rs +++ b/utilities/traffic-sim/src/sim.rs @@ -76,7 +76,10 @@ impl Wallet { /// A validity window ending [`SIM_TRANSACTION_VALIDITY_EPOCHS`] past the daemon's current epoch. pub async fn max_epoch(&mut self) -> anyhow::Result { let settings = self.client.get_settings().await?; - Ok(Epoch(settings.current_epoch.as_u64() + SIM_TRANSACTION_VALIDITY_EPOCHS)) + let current_epoch = settings + .current_epoch + .ok_or_else(|| anyhow::anyhow!("Wallet daemon could not reach its indexer, so the epoch is unknown"))?; + Ok(Epoch(current_epoch.as_u64() + SIM_TRANSACTION_VALIDITY_EPOCHS)) } pub async fn connect(name: String, address: &str) -> anyhow::Result { From 8d4f7563349512403b0c194c011f132ba615eb8f Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Fri, 14 Aug 2026 13:46:29 +0400 Subject: [PATCH 4/8] fix(walletd): address review of the mandatory max_epoch window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auto-claim burn no longer re-queries the epoch to stamp `max_epoch`. It takes the epoch `check_and_submit_pending` already resolved, so a momentary indexer outage can no longer be classified `Permanent` and drop a burn from the queue for good. The round-trip goes away with it. - Restore `MAX_RETRIES_NETWORK`'s doc comment, which the new validity-window constant had been inserted underneath. - Drop the now-stale `#[serde(default)]` on `TransactionPoolRecord::max_epoch`. With the field mandatory, `Epoch: Default` is `Epoch(0)`, so a payload missing it went from meaning "no expiry" to "expired before epoch 1" — the attribute inverts the meaning it was added for. A missing field is now an error. - Validate `default_transaction_validity_epochs` at startup. Zero is refused: it stamps `max_epoch == current_epoch`, so a transaction built near a boundary expires before it can land. An oversized value is only warned about — the network's ceiling decides, and the wallet does not depend on the consensus crate, so it has no authority to refuse a window a larger network would accept. - traffic-sim resolves its validity window per wallet rather than once per run. Each loop iteration makes several daemon round-trips, so a single window taken up front could lapse before the later wallets were funded. Co-Authored-By: Claude Opus 5 --- applications/tari_walletd/src/config.rs | 34 +++++++++++++++++++ applications/tari_walletd/src/lib.rs | 2 ++ .../src/services/auto_claim_burn_service.rs | 33 ++++++++++-------- .../src/consensus_models/transaction_pool.rs | 1 - utilities/traffic-sim/src/sim.rs | 10 +++--- 5 files changed, 60 insertions(+), 20 deletions(-) diff --git a/applications/tari_walletd/src/config.rs b/applications/tari_walletd/src/config.rs index 5328c4a9db..b947fbf749 100644 --- a/applications/tari_walletd/src/config.rs +++ b/applications/tari_walletd/src/config.rs @@ -113,6 +113,32 @@ pub struct WalletDaemonConfig { } impl WalletDaemonConfig { + /// Rejects a configuration that cannot build a usable transaction. + /// + /// A zero window stamps `max_epoch == current_epoch`, so every transaction the daemon builds + /// must be sequenced within the epoch it was built in and one built near a boundary expires + /// before it can land. An oversized window is only warned about: it is the network's ceiling + /// that decides, and this process cannot read it. + pub fn validate(&self) -> Result<(), anyhow::Error> { + if self.default_transaction_validity_epochs == 0 { + return Err( + anyhow::anyhow!( + "default_transaction_validity_epochs must be at least 1: a zero window expires transactions in \ + the epoch they are built in" + ), + ); + } + if self.default_transaction_validity_epochs > IMPLAUSIBLE_TRANSACTION_VALIDITY_EPOCHS { + log::warn!( + target: LOG_TARGET, + "default_transaction_validity_epochs ({}) is beyond the network transaction validity ceiling known to this build ({}). If the network enforces that ceiling, every transaction this wallet builds will be refused as out of range.", + self.default_transaction_validity_epochs, + IMPLAUSIBLE_TRANSACTION_VALIDITY_EPOCHS, + ); + } + Ok(()) + } + pub fn get_burn_proof_dir(&self, network: Network) -> PathBuf { self.burn_proof_dir .clone() @@ -130,6 +156,14 @@ fn return_default_transaction_validity_epochs() -> u64 { 3 } +/// The network's `max_transaction_validity_epochs` at the time of writing. The wallet does not +/// depend on the consensus crate, so it cannot read the live value and treats this only as the +/// threshold for warning an operator that their window looks implausible — never as a hard limit, +/// which would wrongly refuse to start against a network configured with a larger ceiling. +const IMPLAUSIBLE_TRANSACTION_VALIDITY_EPOCHS: u64 = 2160; + +const LOG_TARGET: &str = "tari::ootle::wallet_daemon::config"; + fn return_default_transaction_request_ttl() -> Duration { Duration::from_secs(30 * 60) } diff --git a/applications/tari_walletd/src/lib.rs b/applications/tari_walletd/src/lib.rs index b293399ad8..532b5a3006 100644 --- a/applications/tari_walletd/src/lib.rs +++ b/applications/tari_walletd/src/lib.rs @@ -84,6 +84,8 @@ pub async fn run_tari_ootle_walletd( // Uncomment to enable tokio tracing via tokio-console // console_subscriber::init(); + config.ootle_wallet_daemon.validate()?; + let wallet_store = init_wallet_store(&config)?; let mut wallet_sdk: WalletSdk = initialize_wallet_sdk(&config, wallet_store.clone())?; diff --git a/applications/tari_walletd/src/services/auto_claim_burn_service.rs b/applications/tari_walletd/src/services/auto_claim_burn_service.rs index 316abc2bb3..bb072b285f 100644 --- a/applications/tari_walletd/src/services/auto_claim_burn_service.rs +++ b/applications/tari_walletd/src/services/auto_claim_burn_service.rs @@ -31,11 +31,10 @@ use crate::{ const LOG_TARGET: &str = "tari::ootle::wallet_daemon::auto_claim_burn"; const EPOCH_CHECK_INTERVAL: Duration = Duration::from_secs(30); -/// Maximum retries for network/submission errors (indexer unreachable, tx service down). /// Validity window for an unattended claim: a few epochs is ample for the submit itself, and a /// claim that does not land in that time is retried with a fresh window. const CLAIM_TRANSACTION_VALIDITY_EPOCHS: u64 = 3; - +/// Maximum retries for network/submission errors (indexer unreachable, tx service down). const MAX_RETRIES_NETWORK: u32 = 10; /// Maximum retries for file read/parse errors (file still being written on macOS). const MAX_RETRIES_FILE_READ: u32 = 1; @@ -344,7 +343,7 @@ impl AutoClaimBurnService { .collect(); for file_name in ready { - match self.try_submit_claim(&file_name).await { + match self.try_submit_claim(&file_name, current_epoch).await { Ok(tx_id) => { info!( target: LOG_TARGET, @@ -433,7 +432,12 @@ impl AutoClaimBurnService { .with_context(|| format!("Failed to parse burn proof file: {}", path.display())) } - async fn try_submit_claim(&self, file_name: &str) -> Result { + async fn try_submit_claim( + &self, + file_name: &str, + current_epoch: Epoch, + ) -> Result { + let max_epoch = claim_max_epoch(current_epoch); let complete_proof = self .read_proof_file(file_name) .await @@ -462,7 +466,7 @@ impl AutoClaimBurnService { &account, proof_contents.clone(), 1, - self.claim_max_epoch().await.map_err(ClaimError::Permanent)?, + max_epoch, true, Some(file_name.to_string()), ) @@ -505,7 +509,7 @@ impl AutoClaimBurnService { &account, proof_contents, required_fees, - self.claim_max_epoch().await.map_err(ClaimError::Permanent)?, + max_epoch, false, Some(file_name.to_string()), ) @@ -515,15 +519,6 @@ impl AutoClaimBurnService { Ok(resp.transaction_id) } - /// The validity window stamped on claim transactions this service builds. Claims are submitted - /// unattended, so the window only has to cover the submission itself. - async fn claim_max_epoch(&self) -> anyhow::Result { - let current = self.query_current_epoch().await?; - Ok(Epoch( - current.as_u64().saturating_add(CLAIM_TRANSACTION_VALIDITY_EPOCHS), - )) - } - async fn query_current_epoch(&self) -> anyhow::Result { self.sdk .get_network_interface() @@ -557,6 +552,14 @@ impl PendingClaim { } /// Categorises errors to determine retry behaviour for auto-claims. +/// The validity window stamped on claim transactions this service builds. Claims are submitted +/// unattended, so the window only has to cover the submission itself. Derived from the epoch the +/// caller already resolved: re-querying it here would turn a momentary indexer outage into a +/// permanent claim failure. +fn claim_max_epoch(current_epoch: Epoch) -> Epoch { + Epoch(current_epoch.as_u64().saturating_add(CLAIM_TRANSACTION_VALIDITY_EPOCHS)) +} + enum ClaimError { /// A permanent error (account not in this wallet, invalid proof data). Remove from queue; the /// proof file remains in `burn_proof_dir` for the user to inspect and retry manually. diff --git a/crates/storage/src/consensus_models/transaction_pool.rs b/crates/storage/src/consensus_models/transaction_pool.rs index 7ea2eac94a..987b28ee0e 100644 --- a/crates/storage/src/consensus_models/transaction_pool.rs +++ b/crates/storage/src/consensus_models/transaction_pool.rs @@ -414,7 +414,6 @@ pub struct TransactionPoolRecord { #[n(10)] is_ready: bool, /// The maximum epoch for which this transaction is valid. - #[serde(default)] #[n(11)] max_epoch: Epoch, /// Epoch to use when executing the transaction. This updates as foreign proposals are received diff --git a/utilities/traffic-sim/src/sim.rs b/utilities/traffic-sim/src/sim.rs index d3e9f1e2b3..d4ea461360 100644 --- a/utilities/traffic-sim/src/sim.rs +++ b/utilities/traffic-sim/src/sim.rs @@ -74,8 +74,8 @@ const SIM_TRANSACTION_VALIDITY_EPOCHS: u64 = 10; impl Wallet { /// A validity window ending [`SIM_TRANSACTION_VALIDITY_EPOCHS`] past the daemon's current epoch. - pub async fn max_epoch(&mut self) -> anyhow::Result { - let settings = self.client.get_settings().await?; + pub async fn max_epoch(&self) -> anyhow::Result { + let settings = self.client.clone().get_settings().await?; let current_epoch = settings .current_epoch .ok_or_else(|| anyhow::anyhow!("Wallet daemon could not reach its indexer, so the epoch is unknown"))?; @@ -425,10 +425,12 @@ impl TrafficSim { .owner_key_id .ok_or_else(|| anyhow::anyhow!("Exchange account has no owner key ID"))?; - let max_epoch = exchange_wallet.max_epoch().await?; - for (wallet, account) in self.wallet_and_account_iter() { let mut client = wallet.client.clone(); + // Resolved per wallet, not once for the run: each iteration makes several daemon + // round-trips, so a window taken at the start can lapse before the later wallets are + // funded. + let max_epoch = wallet.max_epoch().await?; let fund_amount = 100_000_000_000u64; // 1000 coins From 6e951f88457a3f9902c985b3c697cdb2aa3d5a26 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Fri, 14 Aug 2026 13:57:14 +0400 Subject: [PATCH 5/8] fix(walletd): repair mangled config validation strings --- applications/tari_walletd/src/config.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/applications/tari_walletd/src/config.rs b/applications/tari_walletd/src/config.rs index b947fbf749..8cadd22dda 100644 --- a/applications/tari_walletd/src/config.rs +++ b/applications/tari_walletd/src/config.rs @@ -121,17 +121,17 @@ impl WalletDaemonConfig { /// that decides, and this process cannot read it. pub fn validate(&self) -> Result<(), anyhow::Error> { if self.default_transaction_validity_epochs == 0 { - return Err( - anyhow::anyhow!( - "default_transaction_validity_epochs must be at least 1: a zero window expires transactions in \ - the epoch they are built in" - ), - ); + return Err(anyhow::anyhow!( + "default_transaction_validity_epochs must be at least 1: a zero window expires transactions in the \ + epoch they are built in" + )); } if self.default_transaction_validity_epochs > IMPLAUSIBLE_TRANSACTION_VALIDITY_EPOCHS { log::warn!( target: LOG_TARGET, - "default_transaction_validity_epochs ({}) is beyond the network transaction validity ceiling known to this build ({}). If the network enforces that ceiling, every transaction this wallet builds will be refused as out of range.", + "default_transaction_validity_epochs ({}) is beyond the transaction validity ceiling known to this \ + build ({}). If the network enforces that ceiling, every transaction this wallet builds will be \ + refused as out of range.", self.default_transaction_validity_epochs, IMPLAUSIBLE_TRANSACTION_VALIDITY_EPOCHS, ); From 83999857bbf891251cc98288ddb697452f4c0463 Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Fri, 14 Aug 2026 14:38:15 +0400 Subject: [PATCH 6/8] lints --- crates/engine/tests/confidential.rs | 8 ++++---- crates/engine/tests/stealth.rs | 4 ++-- crates/transaction_validation/src/blob_references.rs | 3 ++- crates/wallet/sdk_tests/tests/transaction_api.rs | 3 ++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/engine/tests/confidential.rs b/crates/engine/tests/confidential.rs index 21154740ad..5315035ae9 100644 --- a/crates/engine/tests/confidential.rs +++ b/crates/engine/tests/confidential.rs @@ -120,7 +120,7 @@ fn minting_a_commitment_without_a_value_proof_is_rejected() { let commitment = commit_amount(&mint_mask, Amount::from(42u64)).unwrap().to_byte_type(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + test.transaction() .call_method(faucet, "mint_more", args![mint_proof, ValueProofs::new()]) .build_and_seal(test.secret_key()), vec![owner], @@ -784,7 +784,7 @@ fn minting_and_burning_a_commitment_tracks_total_supply() { // Move the whole 500 out of the commitment and into revealed funds. let reveal_proof = generate_reveal_proof(&mask, 500); test.execute_expect_success( - Transaction::builder_localnet() + test.transaction() .call_method(faucet, "take_free_coins", args![reveal_proof]) .put_last_instruction_output_on_workspace("coins") .call_function(utilities, "burn_bucket", args![Workspace("coins")]) @@ -809,7 +809,7 @@ fn burning_a_bucket_holding_commitments_without_a_value_proof_is_rejected() { .unwrap() .to_byte_type(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + test.transaction() .call_method(faucet, "take_free_coins", args![withdraw_proof.proof]) .put_last_instruction_output_on_workspace("coins") .call_function(utilities, "burn_bucket", args![Workspace("coins")]) @@ -846,7 +846,7 @@ fn burning_a_bucket_holding_commitments_with_value_proofs_decreases_total_supply )]); test.execute_expect_success( - Transaction::builder_localnet() + test.transaction() .call_method(faucet, "take_free_coins", args![withdraw_proof.proof]) .put_last_instruction_output_on_workspace("coins") .call_function(utilities, "burn_bucket_with_value_proofs", args![ diff --git a/crates/engine/tests/stealth.rs b/crates/engine/tests/stealth.rs index 3fb00aa4af..0682d049aa 100644 --- a/crates/engine/tests/stealth.rs +++ b/crates/engine/tests/stealth.rs @@ -769,7 +769,7 @@ fn burn_with_elgamal_value_proof_adjusts_supply() { let owner = test.owner_proof(); test.execute_expect_success( - Transaction::builder_localnet() + test.transaction() .call_method(faucet, "burn_utxos", args![vec![(utxo_id, proof)]]) .build_and_seal(test.secret_key()), vec![owner], @@ -814,7 +814,7 @@ fn burn_rejects_elgamal_value_proof_for_a_false_value() { let owner = test.owner_proof(); let reason = test.execute_expect_failure( - Transaction::builder_localnet() + test.transaction() .call_method(faucet, "burn_utxos", args![vec![(utxo_id, proof)]]) .build_and_seal(test.secret_key()), vec![owner], diff --git a/crates/transaction_validation/src/blob_references.rs b/crates/transaction_validation/src/blob_references.rs index bd8eee163b..2a48123d84 100644 --- a/crates/transaction_validation/src/blob_references.rs +++ b/crates/transaction_validation/src/blob_references.rs @@ -45,6 +45,7 @@ impl Validator for BlobReferenceValidator { #[cfg(test)] mod tests { use indexmap::IndexSet; + use tari_engine_types::Epoch; use tari_ootle_transaction::{ Blob, Blobs, @@ -86,7 +87,7 @@ mod tests { instructions, IndexSet::new(), None, - None, + Epoch(100), false, ); unsigned.blobs = Blobs::from_vec(blobs); diff --git a/crates/wallet/sdk_tests/tests/transaction_api.rs b/crates/wallet/sdk_tests/tests/transaction_api.rs index 88784fda48..b48aa0bde9 100644 --- a/crates/wallet/sdk_tests/tests/transaction_api.rs +++ b/crates/wallet/sdk_tests/tests/transaction_api.rs @@ -8,6 +8,7 @@ use std::time::Duration; use tari_consensus_types::Decision; use tari_crypto::ristretto::RistrettoSecretKey; use tari_engine_types::{ + Epoch, commit_result::{AbortReason, ExecuteResult, FinalizeResult, RejectReason, TransactionResult}, fees::{FeeBreakdown, FeeReceipt, FeeSource}, substate::SubstateDiff, @@ -28,7 +29,7 @@ fn now() -> PrimitiveDateTime { } fn build_transaction() -> Transaction { - Transaction::builder_localnet() + Transaction::builder_localnet(Epoch(100)) .allocate_component_address("component") .put_last_instruction_output_on_workspace("bucket") .call_method("component", "new", args!["bucket"]) From c0e9538e7e788b839ba675317a3ac914fb8c2bbc Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Fri, 14 Aug 2026 15:10:43 +0400 Subject: [PATCH 7/8] fix(consensus)!: enforce the validity ceiling against the pinned epoch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Motivation The ceiling on how far ahead `max_epoch` may sit was checked at the consensus new-transaction gate against each node's own epoch view. Unlike the expiry and min-epoch rules — which fail permissively for a node whose view lags — this rule fails *strictly*: a node an epoch behind computes a lower ceiling and refuses a window a node ahead of it accepts. Shard groups routinely lag one another by an epoch, so a lagging group would reject a transaction another group had already sequenced. `validate_new_transaction` returns `Ok(None)`, so the transaction was never pooled and never retried, and the sequencing group waited on foreign pledges that could not arrive until the lagging group caught up. It bit exactly the case the ceiling exists for: a long-window transaction stamped at precisely `current + max_transaction_validity_epochs`. ## Changes - Split the ceiling out of `EpochRangeValidator` into `TransactionValidityWindowValidator`. `EpochRangeValidator` keeps only the rules that are safe wherever a transaction is admitted, and is what the consensus sequencing path runs. - Mempool ingress composes both: a drop there is harmless because nothing has been sequenced, and it is what keeps out-of-window traffic out of the network. - The binding enforcement moves to execution, against the pinned `LockedEpoch` that shard groups agree on before executing. Every node evaluates the same number and reaches the same verdict, so an out-of-window transaction is sequenced as an abort — which all groups adopt — instead of being dropped by whichever group was behind. New `AbortReason::ValidityWindowTooLong`. - `MaxEpochTooFarAhead` is now sender fault beyond twice the ceiling. Near the ceiling it stays node-local so a lagging node cannot graylist an honest peer; past twice it no epoch view reconciles the value, and leaving it unpunished let a flood of unbounded windows extract full structural and signature validation from every node for free. - Wallet daemon: submitting a caller-supplied transaction no longer needs the indexer. `transaction_builder_from_unsigned` (backed by `TransactionBuilder::from_unsigned`) takes network and `max_epoch` from the transaction itself, so `transactions.submit`, `detect_inputs` and the dry run no longer fetch an epoch they immediately discard. - Wallet daemon: `settings.set` invalidates the cached epoch when the indexer changes, so a window is never derived from the previous chain's epoch. - The config warning threshold is pinned to `ConsensusConstants::max_transaction_validity_epochs` by a test, via a dev-dependency so the wallet binary does not carry the consensus crate. ## Deployment notes Consensus-breaking: `AbortReason` gains a variant (proto `VALIDITY_WINDOW_TOO_LONG = 10`) and an out-of-window transaction now aborts at execution rather than being refused at admission. ## Verification `cargo nextest --release --all-features` → 1813 passed, 0 failed. clippy and `fmt --check` clean on the CI-pinned nightly. Co-Authored-By: Claude Opus 5 --- Cargo.lock | 1 + .../tari_validator_node/src/bootstrap.rs | 7 +- applications/tari_walletd/Cargo.toml | 3 + applications/tari_walletd/src/config.rs | 44 ++++++++++- .../tari_walletd/src/handlers/context.rs | 24 +++++- .../tari_walletd/src/handlers/settings.rs | 2 + .../tari_walletd/src/handlers/transaction.rs | 12 +-- .../hotstuff/transaction_manager/manager.rs | 44 ++++++++--- crates/consensus/src/hotstuff/worker.rs | 5 +- crates/engine_types/src/commit_result.rs | 4 + crates/ootle_sdk_core/src/types/result.rs | 11 ++- crates/p2p/proto/consensus.proto | 1 + crates/p2p/src/conversions/consensus.rs | 2 + crates/transaction/src/builder/mod.rs | 7 ++ .../transaction_validation/src/epoch_range.rs | 74 +++++++++++++++---- crates/transaction_validation/src/error.rs | 54 +++++++++++++- 16 files changed, 246 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b42cdafbee..1888a1906b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12214,6 +12214,7 @@ dependencies = [ "sha2 0.10.9", "tari_common", "tari_common_types", + "tari_consensus", "tari_crypto", "tari_engine", "tari_engine_types", diff --git a/applications/tari_validator_node/src/bootstrap.rs b/applications/tari_validator_node/src/bootstrap.rs index f72b2b4846..e165848828 100644 --- a/applications/tari_validator_node/src/bootstrap.rs +++ b/applications/tari_validator_node/src/bootstrap.rs @@ -89,6 +89,7 @@ use tari_ootle_transaction_validation::{ TransactionNetworkValidator, TransactionSignatureValidator, TransactionValidationError, + TransactionValidityWindowValidator, TransactionWeightValidator, Validator, }; @@ -362,7 +363,7 @@ pub async fn spawn_services( let transaction_executor = TariBlockTransactionExecutor::new(transaction_processor, consensus_constants.clone()); let transaction_validator = TariBlockTransactionValidator::new( create_mempool_transaction_validator(config.network, template_provider.clone()).boxed(), - EpochRangeValidator::new(consensus_constants.max_transaction_validity_epochs).boxed(), + EpochRangeValidator::new().boxed(), ); #[cfg(feature = "metrics")] @@ -398,7 +399,9 @@ pub async fn spawn_services( let (mempool, join_handle) = mempool::spawn( epoch_manager.clone(), create_mempool_transaction_validator(config.network, template_provider.clone()), - EpochRangeValidator::new(consensus_constants.max_transaction_validity_epochs), + EpochRangeValidator::new().and_then(TransactionValidityWindowValidator::new( + consensus_constants.max_transaction_validity_epochs, + )), state_store.clone(), consensus_handle.clone(), networking.clone(), diff --git a/applications/tari_walletd/Cargo.toml b/applications/tari_walletd/Cargo.toml index 496a54ff2e..b20724f9c2 100644 --- a/applications/tari_walletd/Cargo.toml +++ b/applications/tari_walletd/Cargo.toml @@ -93,6 +93,9 @@ dbus-secret-service-keyring-store = { workspace = true, features = ["crypto-rust windows-native-keyring-store = { workspace = true } [dev-dependencies] +# Test-only, so the wallet binary does not carry the consensus engine: lets the config guard +# assert against the real network ceiling rather than drifting from a hand-copied number. +tari_consensus = { workspace = true } tari_utilities = { workspace = true } [package.metadata.cargo-machete] diff --git a/applications/tari_walletd/src/config.rs b/applications/tari_walletd/src/config.rs index 8cadd22dda..856620e51f 100644 --- a/applications/tari_walletd/src/config.rs +++ b/applications/tari_walletd/src/config.rs @@ -156,10 +156,10 @@ fn return_default_transaction_validity_epochs() -> u64 { 3 } -/// The network's `max_transaction_validity_epochs` at the time of writing. The wallet does not -/// depend on the consensus crate, so it cannot read the live value and treats this only as the -/// threshold for warning an operator that their window looks implausible — never as a hard limit, -/// which would wrongly refuse to start against a network configured with a larger ceiling. +/// Mirrors `ConsensusConstants::max_transaction_validity_epochs`. The wallet binary does not carry +/// the consensus crate, so the value is duplicated here and pinned to the real one by a test; it is +/// used only to warn an operator that their configured window looks implausible, never as a hard +/// limit, which would wrongly refuse to start against a network with a larger ceiling. const IMPLAUSIBLE_TRANSACTION_VALIDITY_EPOCHS: u64 = 2160; const LOG_TARGET: &str = "tari::ootle::wallet_daemon::config"; @@ -264,3 +264,39 @@ impl FromStr for WalletDaemonAuth { } } } + +#[cfg(test)] +mod tests { + use tari_consensus::consensus_constants::ConsensusConstants; + + use super::*; + + /// The warning threshold is only useful while it tracks the rule it mirrors. If the consensus + /// ceiling is lowered and this is not, the wallet keeps accepting a window under which every + /// transaction it builds is unsequenceable. + #[test] + fn the_warning_threshold_tracks_the_consensus_ceiling() { + assert_eq!( + IMPLAUSIBLE_TRANSACTION_VALIDITY_EPOCHS, + ConsensusConstants::mainnet().max_transaction_validity_epochs + ); + } + + #[test] + fn a_zero_validity_window_is_refused() { + let config = WalletDaemonConfig { + default_transaction_validity_epochs: 0, + ..Default::default() + }; + assert!(config.validate().is_err()); + } + + #[test] + fn a_window_at_the_ceiling_is_accepted() { + let config = WalletDaemonConfig { + default_transaction_validity_epochs: IMPLAUSIBLE_TRANSACTION_VALIDITY_EPOCHS, + ..Default::default() + }; + config.validate().unwrap(); + } +} diff --git a/applications/tari_walletd/src/handlers/context.rs b/applications/tari_walletd/src/handlers/context.rs index eb70587713..32373e04ba 100644 --- a/applications/tari_walletd/src/handlers/context.rs +++ b/applications/tari_walletd/src/handlers/context.rs @@ -8,7 +8,7 @@ use std::{ use axum_extra::headers::authorization::Bearer; use dashmap::DashMap; -use tari_ootle_transaction::{Epoch, Transaction, TransactionBuilder}; +use tari_ootle_transaction::{Epoch, Transaction, TransactionBuilder, UnsignedTransaction}; use tari_ootle_wallet_sdk::{models::WalletEvent, network::WalletNetworkInterface}; use tari_ootle_wallet_sdk_services::{ account_monitor::AccountMonitorHandle, @@ -295,6 +295,15 @@ impl HandlerContext { self.authenticator.webauthn() } + /// Discards the cached epoch, forcing the next read to go to the network. + /// + /// Must be called whenever the daemon is repointed at a different indexer: the cached value + /// describes the previous indexer's chain, and stamping a `max_epoch` derived from it onto a + /// transaction for a different chain yields a window that chain will not accept. + pub fn invalidate_epoch_cache(&self) { + *self.cached_epoch.lock().unwrap() = None; + } + /// The current epoch, re-read from the network at most every [`EPOCH_CACHE_TTL`]. pub async fn current_epoch(&self) -> Result { if let Some((epoch, read_at)) = *self.cached_epoch.lock().unwrap() && @@ -319,6 +328,19 @@ impl HandlerContext { )) } + /// A builder seeded from a caller-supplied transaction. + /// + /// The caller has already chosen everything this would otherwise resolve — including its own + /// `max_epoch` — so unlike [`Self::transaction_builder`] this needs no epoch and makes no + /// network call. Submitting a pre-built transaction therefore does not depend on the indexer + /// being reachable. + pub fn transaction_builder_from_unsigned>( + &self, + transaction: T, + ) -> TransactionBuilder { + TransactionBuilder::from_unsigned(transaction) + } + /// Returns a TransactionBuilder with the current network configured. /// /// The builder is stamped with a random nonce so that repeated identical intents (same diff --git a/applications/tari_walletd/src/handlers/settings.rs b/applications/tari_walletd/src/handlers/settings.rs index 782352d56b..d2fc61f3ec 100644 --- a/applications/tari_walletd/src/handlers/settings.rs +++ b/applications/tari_walletd/src/handlers/settings.rs @@ -62,6 +62,8 @@ pub async fn handle_set( if let Some(indexer_url) = req.indexer_url { sdk.config_api().set(ConfigKey::IndexerUrl, &indexer_url)?; sdk.get_network_interface().set_endpoint(indexer_url); + // The cached epoch describes the indexer we just stopped using. + context.invalidate_epoch_cache(); } if let Some(advanced_ui_features) = &req.advanced_ui_features { sdk.config_api() diff --git a/applications/tari_walletd/src/handlers/transaction.rs b/applications/tari_walletd/src/handlers/transaction.rs index 61fe62e00a..bcf45d0025 100644 --- a/applications/tari_walletd/src/handlers/transaction.rs +++ b/applications/tari_walletd/src/handlers/transaction.rs @@ -188,9 +188,7 @@ async fn submit_inner( // Signatures collected out of band are attached first; walletd's own // signatures are added on top and the seal signature commits to all of them. let mut transaction = context - .transaction_builder() - .await? - .with_unsigned_transaction(req.transaction) + .transaction_builder_from_unsigned(req.transaction) .with_inputs(detected_inputs) .with_signatures(req.signatures); @@ -384,9 +382,7 @@ pub async fn handle_detect_inputs( .collect::>(); let transaction = context - .transaction_builder() - .await? - .with_unsigned_transaction(req.transaction) + .transaction_builder_from_unsigned(req.transaction) .with_inputs(detected) .build_unsigned(); @@ -436,9 +432,7 @@ async fn submit_dry_run_inner( }; let mut transaction = context - .transaction_builder() - .await? - .with_unsigned_transaction(req.transaction) + .transaction_builder_from_unsigned(req.transaction) .with_inputs(detected_inputs) .with_dry_run(true) .with_signatures(req.signatures); diff --git a/crates/consensus/src/hotstuff/transaction_manager/manager.rs b/crates/consensus/src/hotstuff/transaction_manager/manager.rs index 115db33968..9c954d9f0e 100644 --- a/crates/consensus/src/hotstuff/transaction_manager/manager.rs +++ b/crates/consensus/src/hotstuff/transaction_manager/manager.rs @@ -11,6 +11,7 @@ use tari_engine_types::{ substate::{Substate, SubstateId}, }; use tari_ootle_common_types::{ + Epoch, LockIntent, SubstateRequirement, SubstateRequirementRef, @@ -47,19 +48,44 @@ const LOG_TARGET: &str = "tari::ootle::consensus::hotstuff::block_transaction_ex #[derive(Debug, Clone)] pub struct ConsensusTransactionManager { executor: TExecutor, + max_transaction_validity_epochs: u64, _store: PhantomData, } impl> ConsensusTransactionManager { - pub fn new(executor: TExecutor) -> Self { + pub fn new(executor: TExecutor, max_transaction_validity_epochs: u64) -> Self { Self { executor, + max_transaction_validity_epochs, _store: PhantomData, } } + /// The abort a transaction earns for declaring a window outside the epoch it was pinned to, or + /// `None` if the window is acceptable. + /// + /// Both rules are evaluated against the pinned epoch — agreed across shard groups before + /// execution — so every node reaches the same verdict regardless of how far its own view had + /// lagged when the transaction was admitted. An out-of-window transaction is therefore sequenced + /// as an abort, which all shard groups adopt, rather than being dropped by whichever group + /// happened to be behind. + fn window_abort_reason(&self, pinned_epoch: Epoch, max_epoch: Epoch) -> Option { + if pinned_epoch > max_epoch { + return Some(AbortReason::EpochExpired); + } + let latest_permitted = Epoch( + pinned_epoch + .as_u64() + .saturating_add(self.max_transaction_validity_epochs), + ); + if max_epoch > latest_permitted { + return Some(AbortReason::ValidityWindowTooLong); + } + None + } + pub fn prepare( &self, store: &mut PendingSubstateStore, @@ -220,21 +246,18 @@ impl> execution_epoch: LockedEpoch, pledged_transaction: PledgedTransaction, ) -> Result { - // Abort before execution if the execution epoch exceeds the transaction's max_epoch let max_epoch = pledged_transaction.transaction.transaction().max_epoch(); - if execution_epoch.epoch() > max_epoch { + if let Some(reason) = self.window_abort_reason(execution_epoch.epoch(), max_epoch) { warn!( target: LOG_TARGET, - "⏰ Transaction {} has expired: execution epoch {} exceeds max_epoch {}", + "⏰ Transaction {} is outside its validity window ({reason}): execution epoch {} against max_epoch {}", pledged_transaction.transaction.id(), execution_epoch, max_epoch, ); return Ok(TransactionExecution::abort( pledged_transaction.transaction.id(), - RejectReason::Abort { - reason: AbortReason::EpochExpired, - }, + RejectReason::Abort { reason }, )); } @@ -268,18 +291,17 @@ impl> block: &LeafBlock, execution_locked_epoch: LockedEpoch, ) -> Result { - // Abort before execution if the execution epoch exceeds the transaction's max_epoch let max_epoch = transaction.transaction().max_epoch(); - if execution_locked_epoch.epoch() > max_epoch { + if let Some(reason) = self.window_abort_reason(execution_locked_epoch.epoch(), max_epoch) { warn!( target: LOG_TARGET, - "⏰ Transaction {} has expired: execution epoch {} exceeds max_epoch {}", + "⏰ Transaction {} is outside its validity window ({reason}): execution epoch {} against max_epoch {}", transaction.id(), execution_locked_epoch.epoch(), max_epoch, ); return Ok(TransactionExecution::abort(transaction.id(), RejectReason::Abort { - reason: AbortReason::EpochExpired, + reason, })); } diff --git a/crates/consensus/src/hotstuff/worker.rs b/crates/consensus/src/hotstuff/worker.rs index eb8b3bfe6d..2d5dac41d3 100644 --- a/crates/consensus/src/hotstuff/worker.rs +++ b/crates/consensus/src/hotstuff/worker.rs @@ -157,7 +157,10 @@ impl HotstuffWorker { ProposalVoteCollector::new(state_store.clone(), epoch_manager.clone(), signing_service.clone()); let timeout_vote_collector = TimeoutVoteCollector::new(state_store.clone(), epoch_manager.clone(), signing_service.clone()); - let transaction_manager = ConsensusTransactionManager::new(transaction_executor.clone()); + let transaction_manager = ConsensusTransactionManager::new( + transaction_executor.clone(), + config.consensus_constants.max_transaction_validity_epochs, + ); Self { local_validator_addr: local_validator_addr.clone(), diff --git a/crates/engine_types/src/commit_result.rs b/crates/engine_types/src/commit_result.rs index db29416063..ef368954d2 100644 --- a/crates/engine_types/src/commit_result.rs +++ b/crates/engine_types/src/commit_result.rs @@ -498,6 +498,10 @@ pub enum AbortReason { FeePaymentInMainIntent, #[n(8)] EpochExpired, + /// The transaction's `max_epoch` is further ahead of the epoch it was pinned to than the network + /// permits. Evaluated against the pinned epoch, so every shard group reaches the same verdict. + #[n(9)] + ValidityWindowTooLong, } impl Display for AbortReason { diff --git a/crates/ootle_sdk_core/src/types/result.rs b/crates/ootle_sdk_core/src/types/result.rs index 5aeccc145e..6b18233a3b 100644 --- a/crates/ootle_sdk_core/src/types/result.rs +++ b/crates/ootle_sdk_core/src/types/result.rs @@ -29,6 +29,7 @@ pub fn abort_code(reason: &AbortReason) -> &'static str { AbortReason::InsufficientFeesPaid => "INSUFFICIENT_FEES_PAID", AbortReason::FeePaymentInMainIntent => "FEE_PAYMENT_IN_MAIN_INTENT", AbortReason::EpochExpired => "EPOCH_EXPIRED", + AbortReason::ValidityWindowTooLong => "VALIDITY_WINDOW_TOO_LONG", } } @@ -255,6 +256,7 @@ mod tests { AbortReason::InsufficientFeesPaid, AbortReason::FeePaymentInMainIntent, AbortReason::EpochExpired, + AbortReason::ValidityWindowTooLong, ]; #[test] @@ -340,8 +342,7 @@ mod tests { } } - /// Each of the 9 canonical `AbortReason` variants (incl. `EpochExpired`) surfaces a non-empty, - /// unique stable code. + /// Each of the 10 canonical `AbortReason` variants surfaces a non-empty, unique stable code. #[test] fn abort_codes_are_non_empty_and_unique() { let mut seen = std::collections::HashSet::new(); @@ -350,8 +351,12 @@ mod tests { assert!(!code.is_empty(), "abort code for {reason:?} is empty"); assert!(seen.insert(code), "duplicate abort code {code} for {reason:?}"); } - assert_eq!(seen.len(), 9, "expected exactly 9 canonical abort codes"); + assert_eq!(seen.len(), 10, "expected exactly 10 canonical abort codes"); assert_eq!(abort_code(&AbortReason::EpochExpired), "EPOCH_EXPIRED"); + assert_eq!( + abort_code(&AbortReason::ValidityWindowTooLong), + "VALIDITY_WINDOW_TOO_LONG" + ); } /// The `Abort` arm surfaces the canonical abort sub-code while keeping its top-level `ABORT` code. diff --git a/crates/p2p/proto/consensus.proto b/crates/p2p/proto/consensus.proto index 3d3488a3bd..55f114a052 100644 --- a/crates/p2p/proto/consensus.proto +++ b/crates/p2p/proto/consensus.proto @@ -160,6 +160,7 @@ enum AbortReason { INSUFFICIENT_FEES_PAID = 7; FEE_PAYMENT_IN_MAIN_INTENT = 8; EPOCH_EXPIRED = 9; + VALIDITY_WINDOW_TOO_LONG = 10; } message Evidence { diff --git a/crates/p2p/src/conversions/consensus.rs b/crates/p2p/src/conversions/consensus.rs index 0631178028..c5cb02046b 100644 --- a/crates/p2p/src/conversions/consensus.rs +++ b/crates/p2p/src/conversions/consensus.rs @@ -837,6 +837,7 @@ impl From for proto::consensus::AbortReason { AbortReason::InsufficientFeesPaid => Self::InsufficientFeesPaid, AbortReason::FeePaymentInMainIntent => Self::FeePaymentInMainIntent, AbortReason::EpochExpired => Self::EpochExpired, + AbortReason::ValidityWindowTooLong => Self::ValidityWindowTooLong, } } } @@ -856,6 +857,7 @@ impl TryFrom for AbortReason { proto::consensus::AbortReason::InsufficientFeesPaid => Ok(Self::InsufficientFeesPaid), proto::consensus::AbortReason::FeePaymentInMainIntent => Ok(Self::FeePaymentInMainIntent), proto::consensus::AbortReason::EpochExpired => Ok(Self::EpochExpired), + proto::consensus::AbortReason::ValidityWindowTooLong => Ok(Self::ValidityWindowTooLong), } } } diff --git a/crates/transaction/src/builder/mod.rs b/crates/transaction/src/builder/mod.rs index eeb6a036c7..753b8532c0 100644 --- a/crates/transaction/src/builder/mod.rs +++ b/crates/transaction/src/builder/mod.rs @@ -94,6 +94,13 @@ impl TransactionBuilder { } pub fn with_unsigned_transaction>(self, unsigned_transaction: T) -> Self { + Self::from_unsigned(unsigned_transaction) + } + + /// Starts from a complete unsigned transaction, taking its network and `max_epoch` from the + /// transaction itself. Nothing about the caller's transaction is chosen here, so this needs no + /// prior builder and — unlike [`Transaction::builder`] — no `max_epoch` decided up front. + pub fn from_unsigned>(unsigned_transaction: T) -> Self { let unsigned_transaction = unsigned_transaction.into(); Self { fee_instruction_builder: Some(Box::new(Self::new_fee_builder( diff --git a/crates/transaction_validation/src/epoch_range.rs b/crates/transaction_validation/src/epoch_range.rs index f217d7e580..b854af7eb1 100644 --- a/crates/transaction_validation/src/epoch_range.rs +++ b/crates/transaction_validation/src/epoch_range.rs @@ -9,20 +9,20 @@ use crate::{TransactionValidationError, Validator}; const LOG_TARGET: &str = "tari::ootle::mempool::validators::epoch_range"; -/// Validates a transaction's validity window against the current epoch. +/// Checks a transaction against the epoch window it declares: not before `min_epoch`, not after +/// `max_epoch`. /// -/// `max_validity_epochs` is the consensus-uniform ceiling on how far ahead of the current epoch a -/// transaction's `max_epoch` may sit. Bounding it is what makes a transaction's death deterministic: -/// past `max_epoch` the transaction can never be sequenced, so a wallet can declare it dead and an -/// aborted attempt cannot be retried forever. -#[derive(Debug)] -pub struct EpochRangeValidator { - max_validity_epochs: u64, -} +/// Safe to run wherever a transaction is admitted, including the consensus sequencing path. Both +/// rules fail in the permissive direction for a node whose epoch view lags: a lagging node admits +/// what a node ahead of it would refuse, so it never discards a transaction its committee has +/// already sequenced. The complementary ceiling on how far ahead `max_epoch` may sit is deliberately +/// **not** here — see [`TransactionValidityWindowValidator`]. +#[derive(Debug, Default)] +pub struct EpochRangeValidator; impl EpochRangeValidator { - pub fn new(max_validity_epochs: u64) -> Self { - Self { max_validity_epochs } + pub fn new() -> Self { + Self } } @@ -50,14 +50,46 @@ impl Validator for EpochRangeValidator { }); } + Ok(()) + } +} + +/// Caps how far ahead of the current epoch a transaction's `max_epoch` may sit, bounding every +/// transaction's lifetime. +/// +/// **Admission only.** Unlike [`EpochRangeValidator`] this rule fails in the *strict* direction for +/// a lagging node: a node an epoch behind computes a lower ceiling and refuses a window a node ahead +/// of it accepts. Running it where a transaction can be silently discarded after being sequenced — +/// the consensus new-transaction gate — would let a lagging shard group refuse to admit a +/// transaction another group had already sequenced, stalling it until that group catches up. The +/// binding enforcement therefore happens at execution against the pinned, cross-group-agreed epoch, +/// where every node reaches the same verdict and an out-of-window transaction is sequenced as an +/// abort rather than dropped. +#[derive(Debug)] +pub struct TransactionValidityWindowValidator { + max_validity_epochs: u64, +} + +impl TransactionValidityWindowValidator { + pub fn new(max_validity_epochs: u64) -> Self { + Self { max_validity_epochs } + } +} + +impl Validator for TransactionValidityWindowValidator { + type Context = Epoch; + type Error = TransactionValidationError; + + fn validate(&self, ¤t_epoch: &Epoch, transaction: &Transaction) -> Result<(), TransactionValidationError> { + let max_epoch = transaction.max_epoch(); // Saturating: an overflowing ceiling admits every representable max_epoch, which is the // correct reading of "no epoch is further ahead than the limit allows". let latest_permitted = Epoch(current_epoch.as_u64().saturating_add(self.max_validity_epochs)); if max_epoch > latest_permitted { warn!( target: LOG_TARGET, - "EpochRangeValidator - FAIL: Maximum epoch {max_epoch} is more than {} epochs beyond current epoch \ - {current_epoch}.", + "TransactionValidityWindowValidator - FAIL: Maximum epoch {max_epoch} is more than {} epochs beyond \ + current epoch {current_epoch}.", self.max_validity_epochs ); return Err(TransactionValidationError::MaxEpochTooFarAhead { @@ -107,8 +139,10 @@ mod tests { ) } + /// The pair as composed at mempool ingress: window rules plus the admission ceiling. fn validate(current_epoch: Epoch, transaction: &Transaction) -> Result<(), TransactionValidationError> { - EpochRangeValidator::new(MAX_VALIDITY_EPOCHS).validate(¤t_epoch, transaction) + EpochRangeValidator::new().validate(¤t_epoch, transaction)?; + TransactionValidityWindowValidator::new(MAX_VALIDITY_EPOCHS).validate(¤t_epoch, transaction) } #[test] @@ -145,6 +179,14 @@ mod tests { )); } + /// The consensus sequencing path must never refuse a window for being too far ahead: a lagging + /// node would otherwise discard a transaction another shard group has already sequenced. + #[test] + fn the_sequencing_rules_ignore_the_ceiling() { + let tx = transaction(None, Epoch(u64::MAX)); + EpochRangeValidator::new().validate(&Epoch(1), &tx).unwrap(); + } + #[test] fn the_ceiling_is_inclusive() { let tx = transaction(None, Epoch(MAX_VALIDITY_EPOCHS)); @@ -154,6 +196,8 @@ mod tests { #[test] fn a_ceiling_that_overflows_admits_any_max_epoch() { let tx = transaction(None, Epoch(u64::MAX)); - EpochRangeValidator::new(u64::MAX).validate(&Epoch(1), &tx).unwrap(); + TransactionValidityWindowValidator::new(u64::MAX) + .validate(&Epoch(1), &tx) + .unwrap(); } } diff --git a/crates/transaction_validation/src/error.rs b/crates/transaction_validation/src/error.rs index 935440e0e0..d8f54e488e 100644 --- a/crates/transaction_validation/src/error.rs +++ b/crates/transaction_validation/src/error.rs @@ -122,8 +122,24 @@ impl TransactionValidationError { Self::OutputSubstateExists { .. } | Self::ValidatorFeeClaimEpochInvalid { .. } | Self::CurrentEpochLessThanMinimum { .. } | - Self::CurrentEpochGreaterThanMaximum { .. } | - Self::MaxEpochTooFarAhead { .. } => false, + Self::CurrentEpochGreaterThanMaximum { .. } => false, + + // A window near the ceiling is a disagreement about the current epoch, not misbehaviour: + // a node whose view lags computes a lower ceiling and would otherwise graylist an honest + // peer whose transaction every node ahead of it accepts. Beyond twice the ceiling no + // epoch view reconciles the value — the sender is at fault on any honest reading, and + // saying so is what stops a flood of unbounded windows costing every node full + // structural and signature validation for free. + Self::MaxEpochTooFarAhead { + current_epoch, + max_epoch, + max_validity_epochs, + } => { + let beyond_any_lag = current_epoch + .as_u64() + .saturating_add(max_validity_epochs.saturating_mul(2)); + max_epoch.as_u64() > beyond_any_lag + }, // Properties of the transaction itself, on which every node agrees. Self::NoFeeInstructions { .. } | @@ -173,9 +189,10 @@ mod tests { current_epoch: Epoch(3), max_epoch: Epoch(2), }, + // Just past the ceiling: reconciled by our own view lagging a few epochs. TransactionValidationError::MaxEpochTooFarAhead { current_epoch: Epoch(1), - max_epoch: Epoch(100), + max_epoch: Epoch(12), max_validity_epochs: 10, }, ]; @@ -184,6 +201,37 @@ mod tests { } } + /// A window no epoch view reconciles is misbehaviour, and must cost the sender: otherwise a + /// flood of unbounded windows extracts full structural and signature validation from every node + /// for free. + #[test] + fn a_window_beyond_any_plausible_lag_is_blamed_on_the_sender() { + let err = TransactionValidationError::MaxEpochTooFarAhead { + current_epoch: Epoch(1), + max_epoch: Epoch(u64::MAX), + max_validity_epochs: 10, + }; + assert!(err.is_sender_fault(), "must penalise the sender for: {err}"); + } + + /// The boundary between the two: twice the ceiling is still forgiven, one past it is not. + #[test] + fn the_sender_fault_boundary_is_twice_the_ceiling() { + let at_boundary = TransactionValidationError::MaxEpochTooFarAhead { + current_epoch: Epoch(1), + max_epoch: Epoch(21), + max_validity_epochs: 10, + }; + assert!(!at_boundary.is_sender_fault()); + + let past_boundary = TransactionValidationError::MaxEpochTooFarAhead { + current_epoch: Epoch(1), + max_epoch: Epoch(22), + max_validity_epochs: 10, + }; + assert!(past_boundary.is_sender_fault()); + } + /// Properties of the transaction itself: every honest node reaches the same verdict, so the /// peer that sent it is answerable for it. #[test] From 2c3167bdb8652d51a0c5314e028c071534b3bb3d Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Fri, 14 Aug 2026 15:53:45 +0400 Subject: [PATCH 8/8] refactor(validator-node): compose one structural chain for both validation sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mempool ingress ran two validators side by side — a structural chain and an epoch chain — which the caller had to remember to pair correctly. It now takes a single `Context = Epoch` validator: the structural rules lifted via `map_context`, then the epoch window, then the admission ceiling. The consensus path keeps its structural/epoch separation, for two reasons. It must run a *different* rule set — no validity ceiling, since a lagging shard group would otherwise refuse to admit a transaction another group has already sequenced — and separating the halves lets it re-check only the epoch rules for mempool-originated transactions instead of verifying their signatures a second time. Both sites now build on one `create_structural_transaction_validator`, so the structural rules have a single definition. Co-Authored-By: Claude Opus 5 --- .../tari_validator_node/src/bootstrap.rs | 35 ++++++++++++----- .../src/p2p/services/mempool/initializer.rs | 8 +--- .../src/p2p/services/mempool/service.rs | 15 ++----- crates/transaction_validation/src/lib.rs | 4 ++ crates/transaction_validation/src/noop.rs | 39 +++++++++++++++++++ 5 files changed, 75 insertions(+), 26 deletions(-) create mode 100644 crates/transaction_validation/src/noop.rs diff --git a/applications/tari_validator_node/src/bootstrap.rs b/applications/tari_validator_node/src/bootstrap.rs index e165848828..88f28695de 100644 --- a/applications/tari_validator_node/src/bootstrap.rs +++ b/applications/tari_validator_node/src/bootstrap.rs @@ -36,6 +36,7 @@ use tari_consensus::consensus_constants::ConsensusConstants; #[cfg(not(feature = "metrics"))] use tari_consensus::traits::hooks::NoopHooks; use tari_crypto::tari_utilities::ByteArray; +use tari_engine_types::Epoch; use tari_epoch_manager::{ EpochManagerReader, service::{EpochManagerConfig, EpochManagerHandle}, @@ -81,6 +82,7 @@ use tari_ootle_transaction_validation::{ BasicValidations, BlobReferenceValidator, EpochRangeValidator, + NoopValidator, PublishTemplateLimitValidator, SignatureLimitValidator, StealthTransactionLimitsValidator, @@ -361,8 +363,10 @@ pub async fn spawn_services( ); // The executor resolves the exhaust burn rate for each transaction's execution epoch. let transaction_executor = TariBlockTransactionExecutor::new(transaction_processor, consensus_constants.clone()); + let transaction_validator = TariBlockTransactionValidator::new( - create_mempool_transaction_validator(config.network, template_provider.clone()).boxed(), + create_structural_transaction_validator(config.network, template_provider.clone(), &consensus_constants) + .boxed(), EpochRangeValidator::new().boxed(), ); @@ -398,10 +402,7 @@ pub async fn spawn_services( let (mempool, join_handle) = mempool::spawn( epoch_manager.clone(), - create_mempool_transaction_validator(config.network, template_provider.clone()), - EpochRangeValidator::new().and_then(TransactionValidityWindowValidator::new( - consensus_constants.max_transaction_validity_epochs, - )), + create_mempool_transaction_validator(config.network, template_provider.clone(), &consensus_constants), state_store.clone(), consensus_handle.clone(), networking.clone(), @@ -519,11 +520,11 @@ async fn spawn_p2p_rpc( Ok(handle) } -pub fn create_mempool_transaction_validator( +pub fn create_structural_transaction_validator( network: Network, template_manager: TProvider, -) -> impl Validator { - let max_transaction_weight = ConsensusConstants::from(network).max_transaction_weight; + constants: &ConsensusConstants, +) -> impl Validator + use { TransactionNetworkValidator::new(network) .and_then(TransactionDryRunValidator) .and_then(BasicValidations::new()) @@ -531,7 +532,7 @@ pub fn create_mempool_transaction_validator( // fail at execution, and unreferenced blobs would never fail at all. .and_then(BlobReferenceValidator::new()) // Cheap structural check — reject over-weight transactions before verifying signatures. - .and_then(TransactionWeightValidator::new(max_transaction_weight)) + .and_then(TransactionWeightValidator::new(constants.max_transaction_weight)) // Reject transactions whose aggregate stealth-transfer work exceeds the per-transaction caps before // verifying signatures or executing. .and_then(StealthTransactionLimitsValidator::new()) @@ -542,6 +543,22 @@ pub fn create_mempool_transaction_validator( .and_then(TemplateExistsValidator::new(template_manager)) } +pub fn create_mempool_transaction_validator( + network: Network, + template_manager: TProvider, + constants: &ConsensusConstants, +) -> impl Validator + use { + NoopValidator::::new() + .map_context( + |_| (), + create_structural_transaction_validator(network, template_manager, constants), + ) + .and_then(EpochRangeValidator::new()) + .and_then(TransactionValidityWindowValidator::new( + constants.max_transaction_validity_epochs, + )) +} + async fn create_base_layer_client( network: Network, config: &BaseLayerOracleConfig, diff --git a/applications/tari_validator_node/src/p2p/services/mempool/initializer.rs b/applications/tari_validator_node/src/p2p/services/mempool/initializer.rs index f41e2245e9..4e9593d885 100644 --- a/applications/tari_validator_node/src/p2p/services/mempool/initializer.rs +++ b/applications/tari_validator_node/src/p2p/services/mempool/initializer.rs @@ -39,10 +39,9 @@ use crate::{ const LOG_TARGET: &str = "tari::ootle::validator_node::mempool"; -pub fn spawn( +pub fn spawn( epoch_manager: EpochManagerHandle, transaction_validator: TValidator, - epoch_validator: TEpochValidator, state_store: TStateStore, consensus_handle: ConsensusHandle, networking: NetworkingHandle, @@ -50,9 +49,7 @@ pub fn spawn( #[cfg(feature = "metrics")] metrics_registry: &mut prometheus_client::registry::Registry, ) -> (MempoolHandle, JoinHandle>) where - TValidator: Validator + Send + Sync + 'static, - TEpochValidator: - Validator + Send + Sync + 'static, + TValidator: Validator + Send + Sync + 'static, TStateStore: StateStore + Send + Sync + 'static, { // This channel only needs to be size 1, because each mempool request must wait for a reply and the mempool is @@ -65,7 +62,6 @@ where rx_mempool_request, epoch_manager, transaction_validator, - epoch_validator, state_store, consensus_handle, networking, diff --git a/applications/tari_validator_node/src/p2p/services/mempool/service.rs b/applications/tari_validator_node/src/p2p/services/mempool/service.rs index d224c155c5..9573ff9d45 100644 --- a/applications/tari_validator_node/src/p2p/services/mempool/service.rs +++ b/applications/tari_validator_node/src/p2p/services/mempool/service.rs @@ -53,12 +53,11 @@ const LOG_TARGET: &str = "tari::validator_node::mempool::service"; const MEM_MAX_TRANSACTIONS_DEDUP: usize = 1_000_000; #[derive(Debug)] -pub struct MempoolService { +pub struct MempoolService { transactions: SeenTransactions, mempool_requests: mpsc::Receiver, epoch_manager: EpochManagerHandle, before_execute_validator: TValidator, - epoch_validator: TEpochValidator, state_store: TStateStore, gossip: MempoolGossip, consensus_handle: ConsensusHandle, @@ -66,17 +65,15 @@ pub struct MempoolService { metrics: PrometheusMempoolMetrics, } -impl MempoolService +impl MempoolService where - TValidator: Validator, - TEpochValidator: Validator, + TValidator: Validator, TStateStore: StateStore, { pub(super) fn new( mempool_requests: mpsc::Receiver, epoch_manager: EpochManagerHandle, before_execute_validator: TValidator, - epoch_validator: TEpochValidator, state_store: TStateStore, consensus_handle: ConsensusHandle, networking: NetworkingHandle, @@ -89,7 +86,6 @@ where mempool_requests, epoch_manager, before_execute_validator, - epoch_validator, state_store, consensus_handle, #[cfg(feature = "metrics")] @@ -271,10 +267,7 @@ where // transaction outside its validity window is refused before it is admitted or re-gossiped // rather than after. Both feed the single acceptance verdict below. let current_epoch = self.consensus_handle.current_view().get_epoch(); - let validation_result = self - .before_execute_validator - .validate(&(), &transaction) - .and_then(|_| self.epoch_validator.validate(¤t_epoch, &transaction)); + let validation_result = self.before_execute_validator.validate(¤t_epoch, &transaction); // Reported here rather than at the end of this function: everything below is about whether // *we* act on the transaction, not whether it is valid, and gossipsub only holds a message diff --git a/crates/transaction_validation/src/lib.rs b/crates/transaction_validation/src/lib.rs index ccd49e05e4..d9331de4e4 100644 --- a/crates/transaction_validation/src/lib.rs +++ b/crates/transaction_validation/src/lib.rs @@ -35,6 +35,10 @@ mod template_exists; pub use template_exists::*; mod weight; pub use weight::*; + +mod noop; +pub use noop::*; + mod with_context; pub use with_context::*; diff --git a/crates/transaction_validation/src/noop.rs b/crates/transaction_validation/src/noop.rs new file mode 100644 index 0000000000..54a02e8f54 --- /dev/null +++ b/crates/transaction_validation/src/noop.rs @@ -0,0 +1,39 @@ +// Copyright 2024 The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use std::marker::PhantomData; + +use tari_ootle_transaction::Transaction; + +use crate::Validator; + +/// No Op validator - does nothing. Generic on any context or error +#[derive(Debug)] +pub struct NoopValidator(PhantomData<(Ctx, Err)>); + +impl NoopValidator { + pub const fn new() -> Self { + Self(PhantomData) + } +} + +impl Validator for NoopValidator { + type Context = Ctx; + type Error = Err; + + fn validate(&self, _context: &Ctx, _transaction: &Transaction) -> Result<(), Self::Error> { + Ok(()) + } +} + +impl Clone for NoopValidator { + fn clone(&self) -> Self { + NoopValidator(PhantomData) + } +} + +impl Default for NoopValidator { + fn default() -> Self { + NoopValidator::new() + } +}