Skip to content

Commit 0d11f57

Browse files
committed
[address-balance] support multi-epoch ValidDuring
1 parent 81aa9c5 commit 0d11f57

File tree

12 files changed

+344
-13
lines changed

12 files changed

+344
-13
lines changed

crates/sui-core/src/authority.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::congestion_tracker::CongestionTracker;
1010
use crate::consensus_adapter::ConsensusOverloadChecker;
1111
use crate::execution_cache::ExecutionCacheTraitPointers;
1212
use crate::execution_cache::TransactionCacheRead;
13+
use crate::execution_cache::writeback_cache::WritebackCache;
1314
use crate::execution_scheduler::ExecutionScheduler;
1415
use crate::execution_scheduler::SchedulingSource;
1516
use crate::jsonrpc_index::CoinIndexKey2;
@@ -1260,6 +1261,16 @@ impl AuthorityState {
12601261
return Ok(());
12611262
}
12621263

1264+
if let Ok(true) = self
1265+
.get_transaction_cache_reader()
1266+
.transaction_executed_in_last_epoch(transaction.digest(), epoch_store.epoch())
1267+
{
1268+
return Err(SuiErrorKind::TransactionAlreadyExecuted {
1269+
digest: (*transaction.digest()),
1270+
}
1271+
.into());
1272+
}
1273+
12631274
let result =
12641275
self.handle_transaction_impl(transaction, false /* sign */, epoch_store)?;
12651276
assert!(
@@ -3721,6 +3732,12 @@ impl AuthorityState {
37213732
.database_for_testing()
37223733
}
37233734

3735+
pub fn cache_for_testing(&self) -> &WritebackCache {
3736+
self.execution_cache_trait_pointers
3737+
.testing_api
3738+
.cache_for_testing()
3739+
}
3740+
37243741
pub async fn prune_checkpoints_for_eligible_epochs_for_testing(
37253742
&self,
37263743
config: NodeConfig,

crates/sui-core/src/authority/authority_store.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1597,6 +1597,18 @@ impl AuthorityStore {
15971597
let _ = AuthorityStorePruner::compact(&self.perpetual_tables);
15981598
}
15991599

1600+
pub fn remove_executed_effects_for_testing(
1601+
&self,
1602+
tx_digest: &TransactionDigest,
1603+
) -> anyhow::Result<()> {
1604+
let effects_digest = self.perpetual_tables.executed_effects.get(tx_digest)?;
1605+
if let Some(effects_digest) = effects_digest {
1606+
self.perpetual_tables.executed_effects.remove(tx_digest)?;
1607+
self.perpetual_tables.effects.remove(&effects_digest)?;
1608+
}
1609+
Ok(())
1610+
}
1611+
16001612
#[cfg(test)]
16011613
pub async fn prune_objects_immediately_for_testing(
16021614
&self,

crates/sui-core/src/authority/authority_store_tables.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,19 @@ impl AuthorityPerpetualTables {
633633
Ok(self.effects.get(&effect_digest)?)
634634
}
635635

636+
pub(crate) fn was_transaction_executed_in_last_epoch(
637+
&self,
638+
digest: &TransactionDigest,
639+
current_epoch: EpochId,
640+
) -> SuiResult<bool> {
641+
if current_epoch == 0 {
642+
return Ok(false);
643+
}
644+
Ok(self
645+
.executed_transaction_digests
646+
.contains_key(&(current_epoch - 1, *digest))?)
647+
}
648+
636649
// DEPRECATED as the backing table has been moved to authority_per_epoch_store.
637650
// Please do not add new accessors/callsites.
638651
pub fn get_checkpoint_sequence_number(

crates/sui-core/src/authority_server.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,21 @@ impl ValidatorService {
757757
}
758758
}
759759

760+
if self
761+
.state
762+
.get_transaction_cache_reader()
763+
.transaction_executed_in_last_epoch(tx_digest, epoch_store.epoch())?
764+
{
765+
results[idx] = Some(SubmitTxResult::Rejected {
766+
error: UserInputError::TransactionAlreadyExecuted { digest: *tx_digest }.into(),
767+
});
768+
debug!(
769+
?tx_digest,
770+
"handle_submit_transaction: transaction already executed in previous epoch"
771+
);
772+
continue;
773+
}
774+
760775
debug!(
761776
?tx_digest,
762777
"handle_submit_transaction: waiting for fastpath dependency objects"

crates/sui-core/src/checkpoints/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3757,6 +3757,14 @@ mod tests {
37573757
) -> Option<Vec<sui_types::storage::ObjectKey>> {
37583758
unimplemented!()
37593759
}
3760+
3761+
fn transaction_executed_in_last_epoch(
3762+
&self,
3763+
_: &TransactionDigest,
3764+
_: EpochId,
3765+
) -> SuiResult<bool> {
3766+
unimplemented!()
3767+
}
37603768
}
37613769

37623770
#[async_trait::async_trait]

crates/sui-core/src/execution_cache.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,12 @@ pub trait TransactionCacheRead: Send + Sync {
500500
.expect("multi-get must return correct number of items")
501501
}
502502

503+
fn transaction_executed_in_last_epoch(
504+
&self,
505+
digest: &TransactionDigest,
506+
current_epoch: EpochId,
507+
) -> SuiResult<bool>;
508+
503509
fn multi_get_effects(
504510
&self,
505511
digests: &[TransactionEffectsDigest],
@@ -689,6 +695,8 @@ pub trait StateSyncAPI: Send + Sync {
689695

690696
pub trait TestingAPI: Send + Sync {
691697
fn database_for_testing(&self) -> Arc<AuthorityStore>;
698+
699+
fn cache_for_testing(&self) -> &WritebackCache;
692700
}
693701

694702
macro_rules! implement_storage_traits {
@@ -882,6 +890,10 @@ macro_rules! implement_passthrough_traits {
882890
fn database_for_testing(&self) -> Arc<AuthorityStore> {
883891
self.store.clone()
884892
}
893+
894+
fn cache_for_testing(&self) -> &WritebackCache {
895+
self
896+
}
885897
}
886898
};
887899
}

crates/sui-core/src/execution_cache/writeback_cache.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,12 @@ impl WritebackCache {
549549
std::mem::swap(self, &mut new);
550550
}
551551

552+
pub fn evict_executed_effects_from_cache_for_testing(&self, tx_digest: &TransactionDigest) {
553+
self.cached.executed_effects_digests.invalidate(tx_digest);
554+
self.cached.transaction_events.invalidate(tx_digest);
555+
self.cached.transactions.invalidate(tx_digest);
556+
}
557+
552558
fn write_object_entry(
553559
&self,
554560
object_id: &ObjectID,
@@ -2082,6 +2088,16 @@ impl TransactionCacheRead for WritebackCache {
20822088
)
20832089
}
20842090

2091+
fn transaction_executed_in_last_epoch(
2092+
&self,
2093+
digest: &TransactionDigest,
2094+
current_epoch: EpochId,
2095+
) -> SuiResult<bool> {
2096+
self.store
2097+
.perpetual_tables
2098+
.was_transaction_executed_in_last_epoch(digest, current_epoch)
2099+
}
2100+
20852101
fn notify_read_executed_effects_digests<'a>(
20862102
&'a self,
20872103
task_name: &'static str,

crates/sui-e2e-tests/tests/address_balance_tests.rs

Lines changed: 202 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
12511293
fn 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

Comments
 (0)