Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 718049a

Browse files
miloserdowwacban
authored andcommittedMar 25, 2025
runtime: more regression tests for Runtime::apply (#13170)
During the development of parallel transaction processing (#12983), we encountered a bug when multiple transactions from the same account using different keys were included in a single chunk. In this scenario, the account balance was calculated incorrectly because these transactions were processed in parallel. This PR adds two new tests: one to verify the correct ordering of transaction outcomes and another to validate transactions signed with different keys from the same account.
1 parent ab81e5c commit 718049a

File tree

1 file changed

+253
-19
lines changed

1 file changed

+253
-19
lines changed
 

Diff for: ‎runtime/runtime/src/tests/apply.rs

+253-19
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,41 @@ fn setup_runtime(
6161
let epoch_info_provider = MockEpochInfoProvider::default();
6262
let shard_layout = epoch_info_provider.shard_layout(&EpochId::default()).unwrap();
6363
let shard_uid = shard_layout.shard_uids().next().unwrap();
64+
65+
let accounts_with_keys = initial_accounts
66+
.into_iter()
67+
.map(|account_id| {
68+
let signer = Arc::new(InMemorySigner::test_signer(&account_id));
69+
(account_id, vec![signer])
70+
})
71+
.collect::<Vec<_>>();
72+
6473
let (runtime, tries, state_root, apply_state, signers) = setup_runtime_for_shard(
65-
initial_accounts,
74+
accounts_with_keys,
75+
initial_balance,
76+
initial_locked,
77+
gas_limit,
78+
shard_uid,
79+
&shard_layout,
80+
);
81+
82+
(runtime, tries, state_root, apply_state, signers, epoch_info_provider)
83+
}
84+
85+
/// Same general idea as `setup_runtime`, but you can pass multiple keys
86+
/// for each account.
87+
fn setup_runtime_with_keys(
88+
accounts_with_keys: Vec<(AccountId, Vec<Arc<Signer>>)>,
89+
initial_balance: Balance,
90+
initial_locked: Balance,
91+
gas_limit: Gas,
92+
) -> (Runtime, ShardTries, CryptoHash, ApplyState, Vec<Arc<Signer>>, impl EpochInfoProvider) {
93+
let epoch_info_provider = MockEpochInfoProvider::default();
94+
let shard_layout = epoch_info_provider.shard_layout(&EpochId::default()).unwrap();
95+
let shard_uid = shard_layout.shard_uids().next().unwrap();
96+
97+
let (runtime, tries, state_root, apply_state, signers) = setup_runtime_for_shard(
98+
accounts_with_keys,
6699
initial_balance,
67100
initial_locked,
68101
gas_limit,
@@ -74,7 +107,7 @@ fn setup_runtime(
74107
}
75108

76109
fn setup_runtime_for_shard(
77-
initial_accounts: Vec<AccountId>,
110+
accounts_with_keys: Vec<(AccountId, Vec<Arc<Signer>>)>,
78111
initial_balance: Balance,
79112
initial_locked: Balance,
80113
gas_limit: Gas,
@@ -84,23 +117,33 @@ fn setup_runtime_for_shard(
84117
let tries = TestTriesBuilder::new().build();
85118
let root = MerkleHash::default();
86119
let runtime = Runtime::new();
87-
let mut signers = vec![];
88120
let mut initial_state = tries.new_trie_update(shard_uid, root);
89-
for account_id in initial_accounts.into_iter() {
90-
let signer: Arc<Signer> = Arc::new(InMemorySigner::test_signer(&account_id));
91-
let mut initial_account = account_new(initial_balance, CryptoHash::default());
92-
// For the account and a full access key
93-
initial_account.set_storage_usage(182);
94-
initial_account.set_locked(initial_locked);
95-
set_account(&mut initial_state, account_id.clone(), &initial_account);
96-
set_access_key(
97-
&mut initial_state,
98-
account_id,
99-
signer.public_key(),
100-
&AccessKey::full_access(),
101-
);
102-
signers.push(signer);
103-
}
121+
122+
let signers = accounts_with_keys
123+
.into_iter()
124+
.flat_map(|(account_id, signers_for_account)| {
125+
let mut initial_account = account_new(initial_balance, CryptoHash::default());
126+
127+
initial_account.set_storage_usage(182);
128+
initial_account.set_locked(initial_locked);
129+
130+
set_account(&mut initial_state, account_id.clone(), &initial_account);
131+
132+
signers_for_account
133+
.into_iter()
134+
.map(|signer| {
135+
set_access_key(
136+
&mut initial_state,
137+
account_id.clone(),
138+
signer.public_key(),
139+
&AccessKey::full_access(),
140+
);
141+
signer
142+
})
143+
.collect::<Vec<_>>()
144+
})
145+
.collect::<Vec<_>>();
146+
104147
initial_state.commit(StateChangeCause::InitialState);
105148
let trie_changes = initial_state.finalize().unwrap().trie_changes;
106149
let mut store_update = tries.store_update();
@@ -2433,8 +2476,17 @@ fn test_congestion_buffering() {
24332476
let deposit = to_yocto(10_000);
24342477
// execute a single receipt per chunk
24352478
let gas_limit = 1;
2479+
2480+
let accounts_with_keys = accounts
2481+
.into_iter()
2482+
.map(|account| {
2483+
let signer = Arc::new(InMemorySigner::test_signer(&account));
2484+
(account, vec![signer])
2485+
})
2486+
.collect::<Vec<_>>();
2487+
24362488
let (runtime, tries, mut root, mut apply_state, _) = setup_runtime_for_shard(
2437-
accounts,
2489+
accounts_with_keys,
24382490
initial_balance,
24392491
initial_locked,
24402492
gas_limit,
@@ -2784,3 +2836,185 @@ fn test_deploy_and_call_local_receipts() {
27842836
ActionErrorKind::FunctionCallError(FunctionCallError::MethodResolveError(_))
27852837
);
27862838
}
2839+
2840+
/// Verifies that valid transactions from multiple accounts are processed in the correct order,
2841+
/// while transactions with an invalid signer are dropped.
2842+
#[test]
2843+
fn test_transaction_ordering_with_apply() {
2844+
let alice_signer = InMemorySigner::test_signer(&alice_account());
2845+
let bob_signer = InMemorySigner::test_signer(&bob_account());
2846+
let alice_invalid_signer = InMemorySigner::from_seed(alice_account(), KeyType::ED25519, "seed");
2847+
2848+
// This transaction should be droped due to invalid signer.
2849+
let alice_invalid_tx = SignedTransaction::send_money(
2850+
1,
2851+
alice_account(),
2852+
alice_account(),
2853+
&alice_invalid_signer,
2854+
100,
2855+
CryptoHash::default(),
2856+
);
2857+
let alice_tx1 = SignedTransaction::send_money(
2858+
1,
2859+
alice_account(),
2860+
alice_account(),
2861+
&alice_signer,
2862+
200,
2863+
CryptoHash::default(),
2864+
);
2865+
let alice_tx2 = SignedTransaction::send_money(
2866+
2,
2867+
alice_account(),
2868+
bob_account(),
2869+
&alice_signer,
2870+
300,
2871+
CryptoHash::default(),
2872+
);
2873+
let bob_tx1 = SignedTransaction::send_money(
2874+
1,
2875+
bob_account(),
2876+
bob_account(),
2877+
&bob_signer,
2878+
400,
2879+
CryptoHash::default(),
2880+
);
2881+
let bob_tx2 = SignedTransaction::send_money(
2882+
2,
2883+
bob_account(),
2884+
alice_account(),
2885+
&bob_signer,
2886+
500,
2887+
CryptoHash::default(),
2888+
);
2889+
let bob_tx3 = SignedTransaction::send_money(
2890+
3,
2891+
bob_account(),
2892+
bob_account(),
2893+
&bob_signer,
2894+
600,
2895+
CryptoHash::default(),
2896+
);
2897+
2898+
let txs = vec![
2899+
bob_tx1.clone(),
2900+
alice_invalid_tx,
2901+
alice_tx1.clone(),
2902+
bob_tx2.clone(),
2903+
alice_tx2.clone(),
2904+
bob_tx3.clone(),
2905+
];
2906+
2907+
let (runtime, tries, root, apply_state, _signers, epoch_info_provider) = setup_runtime(
2908+
vec![alice_account(), bob_account()],
2909+
to_yocto(1_000_000),
2910+
to_yocto(500_000),
2911+
10u64.pow(15),
2912+
);
2913+
2914+
let validity_flags = vec![true; txs.len()];
2915+
let signed_valid_period_txs = SignedValidPeriodTransactions::new(&txs, &validity_flags);
2916+
let apply_result = runtime
2917+
.apply(
2918+
tries.get_trie_for_shard(ShardUId::single_shard(), root),
2919+
&None,
2920+
&apply_state,
2921+
&[],
2922+
signed_valid_period_txs,
2923+
&epoch_info_provider,
2924+
Default::default(),
2925+
)
2926+
.expect("apply should succeed");
2927+
2928+
let expected_order = vec![
2929+
bob_tx1.get_hash(),
2930+
alice_tx1.get_hash(),
2931+
bob_tx2.get_hash(),
2932+
alice_tx2.get_hash(),
2933+
bob_tx3.get_hash(),
2934+
];
2935+
2936+
// Note: The 3 local receipts are generated for valid transactions
2937+
// where signer_id == receiver_id - tx2, tx4, tx6 (not for tx1 as it is dropped).
2938+
assert_eq!(
2939+
apply_result.outcomes.len(),
2940+
8,
2941+
"should have processed 5 transactions and 3 local receipts"
2942+
);
2943+
let tx_outcomes = apply_result.outcomes.iter().take(5).map(|o| o.id).collect::<Vec<_>>();
2944+
assert_eq!(tx_outcomes, expected_order, "outcomes are not in expected sorted order");
2945+
}
2946+
2947+
/// Verifies proper ordering and balance update for transactions signed with multiple keys from one account.
2948+
/// Alice is set up with 3 full-access keys.
2949+
/// Six transactions from Alice to Bob are submitted using various nonces and keys.
2950+
/// The test checks that outcomes are correctly ordered and Alice's final balance is within the expected range.
2951+
#[test]
2952+
fn test_transaction_multiple_access_keys_with_apply() {
2953+
let alice_signer1 = InMemorySigner::from_seed(alice_account(), KeyType::ED25519, "seed1");
2954+
let alice_signer2 = InMemorySigner::from_seed(alice_account(), KeyType::ED25519, "seed2");
2955+
let alice_signer3 = InMemorySigner::from_seed(alice_account(), KeyType::ED25519, "seed3");
2956+
2957+
let send_money_tx = |nonce, key| {
2958+
SignedTransaction::send_money(
2959+
nonce,
2960+
alice_account(),
2961+
bob_account(),
2962+
key,
2963+
to_yocto(1000),
2964+
CryptoHash::default(),
2965+
)
2966+
};
2967+
2968+
let txs = vec![
2969+
send_money_tx(1, &alice_signer1),
2970+
send_money_tx(1, &alice_signer2),
2971+
send_money_tx(1, &alice_signer3),
2972+
send_money_tx(2, &alice_signer3),
2973+
send_money_tx(2, &alice_signer1),
2974+
send_money_tx(3, &alice_signer1),
2975+
];
2976+
2977+
let accounts_with_keys = vec![
2978+
(
2979+
alice_account(),
2980+
vec![Arc::new(alice_signer1), Arc::new(alice_signer2), Arc::new(alice_signer3)],
2981+
),
2982+
(bob_account(), vec![]),
2983+
];
2984+
2985+
let (runtime, tries, root, mut apply_state, _signers, epoch_info_provider) =
2986+
setup_runtime_with_keys(
2987+
accounts_with_keys,
2988+
to_yocto(1_000_000),
2989+
to_yocto(500_000),
2990+
10u64.pow(15),
2991+
);
2992+
2993+
let validity_flags = vec![true; txs.len()];
2994+
let signed_valid_period_txs = SignedValidPeriodTransactions::new(&txs, &validity_flags);
2995+
let apply_result = runtime
2996+
.apply(
2997+
tries.get_trie_for_shard(ShardUId::single_shard(), root),
2998+
&None,
2999+
&apply_state,
3000+
&[],
3001+
signed_valid_period_txs,
3002+
&epoch_info_provider,
3003+
Default::default(),
3004+
)
3005+
.expect("apply should succeed");
3006+
3007+
let expected_order = txs.iter().map(|tx| tx.get_hash()).collect::<Vec<_>>();
3008+
3009+
assert_eq!(apply_result.outcomes.len(), txs.len(), "should have processed 6 transactions");
3010+
let tx_outcomes = apply_result.outcomes.iter().map(|o| o.id).collect::<Vec<_>>();
3011+
assert_eq!(tx_outcomes, expected_order, "outcomes are not in expected sorted order");
3012+
3013+
let shard_uid = ShardUId::single_shard();
3014+
let root = commit_apply_result(&apply_result, &mut apply_state, &tries, shard_uid);
3015+
let state = tries.new_trie_update(shard_uid, root);
3016+
let account = get_account(&state, &alice_account()).unwrap().unwrap();
3017+
3018+
assert!(account.amount() < to_yocto(994_000));
3019+
assert!(account.amount() > to_yocto(993_000));
3020+
}

0 commit comments

Comments
 (0)
Please sign in to comment.