@@ -1248,6 +1248,48 @@ fn create_withdraw_balance_transaction(
12481248 } )
12491249}
12501250
1251+ fn create_address_balance_transaction_with_expiration (
1252+ sender : SuiAddress ,
1253+ rgp : u64 ,
1254+ min_epoch : Option < u64 > ,
1255+ max_epoch : Option < u64 > ,
1256+ chain_id : ChainIdentifier ,
1257+ nonce : u32 ,
1258+ gas_test_package_id : ObjectID ,
1259+ ) -> TransactionData {
1260+ use sui_types:: transaction:: { GasData , TransactionDataV1 , TransactionExpiration } ;
1261+
1262+ let mut builder = ProgrammableTransactionBuilder :: new ( ) ;
1263+ let value = builder. pure ( 42u64 ) . unwrap ( ) ;
1264+ builder. programmable_move_call (
1265+ gas_test_package_id,
1266+ Identifier :: new ( "gas_test" ) . unwrap ( ) ,
1267+ Identifier :: new ( "create_object_with_storage" ) . unwrap ( ) ,
1268+ vec ! [ ] ,
1269+ vec ! [ value] ,
1270+ ) ;
1271+ let tx = TransactionKind :: ProgrammableTransaction ( builder. finish ( ) ) ;
1272+
1273+ TransactionData :: V1 ( TransactionDataV1 {
1274+ kind : tx,
1275+ sender,
1276+ gas_data : GasData {
1277+ payment : vec ! [ ] ,
1278+ owner : sender,
1279+ price : rgp,
1280+ budget : 10000000 ,
1281+ } ,
1282+ expiration : TransactionExpiration :: ValidDuring {
1283+ min_epoch,
1284+ max_epoch,
1285+ min_timestamp_seconds : None ,
1286+ max_timestamp_seconds : None ,
1287+ chain : chain_id,
1288+ nonce,
1289+ } ,
1290+ } )
1291+ }
1292+
12511293fn create_regular_gas_transaction_with_current_epoch (
12521294 sender : SuiAddress ,
12531295 gas_coin : ObjectRef ,
@@ -1471,8 +1513,8 @@ async fn test_transaction_expiration_min_none_max_some() {
14711513 let err = result. expect_err ( "Transaction should be rejected when only max_epoch is specified" ) ;
14721514 let err_str = format ! ( "{:?}" , err) ;
14731515 assert ! (
1474- err_str. contains( "Both min_epoch and max_epoch must be specified and equal " ) ,
1475- "Expected validation error 'Both min_epoch and max_epoch must be specified and equal ', got: {:?}" ,
1516+ err_str. contains( "Both min_epoch and max_epoch must be specified" ) ,
1517+ "Expected validation error 'Both min_epoch and max_epoch must be specified', got: {:?}" ,
14761518 err
14771519 ) ;
14781520}
@@ -2422,3 +2464,161 @@ async fn test_multiple_deposits_merged_in_effects() {
24222464
24232465 test_cluster. trigger_reconfiguration ( ) . await ;
24242466}
2467+
2468+ #[ sim_test]
2469+ async fn test_reject_transaction_executed_in_previous_epoch ( ) {
2470+ let _guard = ProtocolConfig :: apply_overrides_for_testing ( |_, mut cfg| {
2471+ cfg. enable_address_balance_gas_payments_for_testing ( ) ;
2472+ cfg. enable_accumulators_for_testing ( ) ;
2473+ cfg
2474+ } ) ;
2475+
2476+ let mut test_cluster = TestClusterBuilder :: new ( )
2477+ . with_num_validators ( 1 )
2478+ . build ( )
2479+ . await ;
2480+
2481+ let ( sender, gas_objects) = get_sender_and_all_gas ( & mut test_cluster. wallet ) . await ;
2482+ let rgp = test_cluster. get_reference_gas_price ( ) . await ;
2483+ let chain_id = test_cluster
2484+ . fullnode_handle
2485+ . sui_node
2486+ . with_async ( |node| async { node. state ( ) . get_chain_identifier ( ) } )
2487+ . await ;
2488+
2489+ let current_epoch = test_cluster
2490+ . fullnode_handle
2491+ . sui_node
2492+ . with ( |node| node. state ( ) . epoch_store_for_testing ( ) . epoch ( ) ) ;
2493+
2494+ let gas_test_package_id = setup_test_package ( & mut test_cluster. wallet ) . await ;
2495+
2496+ let fund_tx = make_send_to_account_tx ( 100_000_000 , sender, sender, gas_objects[ 1 ] , rgp) ;
2497+ test_cluster. sign_and_execute_transaction ( & fund_tx) . await ;
2498+
2499+ let tx = create_address_balance_transaction_with_expiration (
2500+ sender,
2501+ rgp,
2502+ Some ( current_epoch) ,
2503+ Some ( current_epoch + 1 ) ,
2504+ chain_id,
2505+ 100 ,
2506+ gas_test_package_id,
2507+ ) ;
2508+
2509+ let signed_tx = test_cluster. sign_transaction ( & tx) . await ;
2510+
2511+ let response = test_cluster
2512+ . wallet
2513+ . execute_transaction_may_fail ( signed_tx. clone ( ) )
2514+ . await
2515+ . unwrap ( ) ;
2516+
2517+ assert ! (
2518+ response. effects. unwrap( ) . status( ) . is_ok( ) ,
2519+ "First execution should succeed"
2520+ ) ;
2521+
2522+ let tx_digest = * signed_tx. digest ( ) ;
2523+
2524+ test_cluster. trigger_reconfiguration ( ) . await ;
2525+
2526+ for handle in test_cluster. swarm . validator_node_handles ( ) {
2527+ handle. with ( |node| {
2528+ node. state ( )
2529+ . database_for_testing ( )
2530+ . remove_executed_effects_for_testing ( & tx_digest)
2531+ . unwrap ( ) ;
2532+ node. state ( )
2533+ . cache_for_testing ( )
2534+ . evict_executed_effects_from_cache_for_testing ( & tx_digest) ;
2535+ } ) ;
2536+ }
2537+
2538+ test_cluster. fullnode_handle . sui_node . with ( |node| {
2539+ node. state ( )
2540+ . database_for_testing ( )
2541+ . remove_executed_effects_for_testing ( & tx_digest)
2542+ . unwrap ( ) ;
2543+ node. state ( )
2544+ . cache_for_testing ( )
2545+ . evict_executed_effects_from_cache_for_testing ( & tx_digest) ;
2546+ } ) ;
2547+
2548+ let result = test_cluster
2549+ . execute_signed_txns_in_soft_bundle ( std:: slice:: from_ref ( & signed_tx) )
2550+ . await ;
2551+
2552+ match result {
2553+ Err ( e) => {
2554+ let err_str = e. to_string ( ) ;
2555+ assert ! (
2556+ err_str. contains( "was already executed" ) ,
2557+ "Expected 'was already executed' error, got: {}" ,
2558+ err_str
2559+ ) ;
2560+ }
2561+ Ok ( _) => {
2562+ panic ! (
2563+ "Transaction should be rejected as already executed in previous epoch, but submission succeeded"
2564+ ) ;
2565+ }
2566+ }
2567+ }
2568+
2569+ #[ sim_test]
2570+ async fn test_transaction_executes_in_next_epoch_with_one_epoch_range ( ) {
2571+ let _guard = ProtocolConfig :: apply_overrides_for_testing ( |_, mut cfg| {
2572+ cfg. enable_address_balance_gas_payments_for_testing ( ) ;
2573+ cfg. enable_accumulators_for_testing ( ) ;
2574+ cfg
2575+ } ) ;
2576+
2577+ let mut test_cluster = TestClusterBuilder :: new ( )
2578+ . with_num_validators ( 1 )
2579+ . build ( )
2580+ . await ;
2581+
2582+ let ( sender, gas_objects) = get_sender_and_all_gas ( & mut test_cluster. wallet ) . await ;
2583+ let rgp = test_cluster. get_reference_gas_price ( ) . await ;
2584+ let chain_id = test_cluster
2585+ . fullnode_handle
2586+ . sui_node
2587+ . with_async ( |node| async { node. state ( ) . get_chain_identifier ( ) } )
2588+ . await ;
2589+
2590+ let current_epoch = test_cluster
2591+ . fullnode_handle
2592+ . sui_node
2593+ . with ( |node| node. state ( ) . epoch_store_for_testing ( ) . epoch ( ) ) ;
2594+
2595+ let gas_test_package_id = setup_test_package ( & mut test_cluster. wallet ) . await ;
2596+
2597+ let fund_tx = make_send_to_account_tx ( 100_000_000 , sender, sender, gas_objects[ 1 ] , rgp) ;
2598+ test_cluster. sign_and_execute_transaction ( & fund_tx) . await ;
2599+
2600+ let tx = create_address_balance_transaction_with_expiration (
2601+ sender,
2602+ rgp,
2603+ Some ( current_epoch) ,
2604+ Some ( current_epoch + 1 ) ,
2605+ chain_id,
2606+ 100 ,
2607+ gas_test_package_id,
2608+ ) ;
2609+
2610+ let signed_tx = test_cluster. sign_transaction ( & tx) . await ;
2611+
2612+ test_cluster. trigger_reconfiguration ( ) . await ;
2613+
2614+ let response = test_cluster
2615+ . wallet
2616+ . execute_transaction_may_fail ( signed_tx. clone ( ) )
2617+ . await
2618+ . unwrap ( ) ;
2619+
2620+ assert ! (
2621+ response. effects. unwrap( ) . status( ) . is_ok( ) ,
2622+ "Transaction with 1-epoch range should execute successfully in next epoch"
2623+ ) ;
2624+ }
0 commit comments