From 5b8fa258197ef90e2251f99441c774c6e491cb9e Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Tue, 25 Aug 2026 16:22:19 +0800 Subject: [PATCH 1/2] perf(keychain): sort scope keys during validation --- .../precompiles/src/account_keychain/mod.rs | 92 ++++++++++++++++++- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/crates/precompiles/src/account_keychain/mod.rs b/crates/precompiles/src/account_keychain/mod.rs index 5da9634ef42..2fc0727bde9 100644 --- a/crates/precompiles/src/account_keychain/mod.rs +++ b/crates/precompiles/src/account_keychain/mod.rs @@ -50,6 +50,12 @@ pub fn is_constrained_tip20_selector(selector: [u8; 4]) -> bool { ) } +fn has_duplicates_sorted(values: impl IntoIterator) -> bool { + let mut values = values.into_iter().collect::>(); + values.sort_unstable(); + values.windows(2).any(|pair| pair[0] == pair[1]) +} + /// Key information stored in the precompile /// /// Storage layout (packed into single slot, right-aligned): @@ -992,6 +998,18 @@ impl AccountKeychain { /// Validates a list of [`CallScope`]s. fn validate_call_scopes(&self, scopes: &[CallScope]) -> Result<()> { + // Preserve the incremental pre-T11 validation order for historical reexecution. + if self.storage.spec().is_t11() { + if has_duplicates_sorted(scopes.iter().map(|scope| scope.target)) { + return Err(AccountKeychainError::invalid_call_scope().into()); + } + + for scope in scopes { + self.validate_call_scope(scope)?; + } + return Ok(()); + } + let mut seen_targets = HashSet::new(); for scope in scopes { if !seen_targets.insert(scope.target) { @@ -1043,9 +1061,14 @@ impl AccountKeychain { } }; + let use_sorted_duplicates = self.storage.spec().is_t11(); + if use_sorted_duplicates && has_duplicates_sorted(rules.iter().map(|rule| rule.selector)) { + return Err(AccountKeychainError::invalid_call_scope().into()); + } + let mut selectors = HashSet::new(); for rule in rules { - if !selectors.insert(rule.selector) { + if !use_sorted_duplicates && !selectors.insert(rule.selector) { return Err(AccountKeychainError::invalid_call_scope().into()); } @@ -1057,11 +1080,19 @@ impl AccountKeychain { return Err(AccountKeychainError::invalid_call_scope().into()); } - let mut unique_recipients = HashSet::new(); - for recipient in &rule.recipients { - if recipient.is_zero() || !unique_recipients.insert(*recipient) { + if use_sorted_duplicates { + if rule.recipients.iter().any(|recipient| recipient.is_zero()) + || has_duplicates_sorted(rule.recipients.iter().copied()) + { return Err(AccountKeychainError::invalid_call_scope().into()); } + } else { + let mut unique_recipients = HashSet::new(); + for recipient in &rule.recipients { + if recipient.is_zero() || !unique_recipients.insert(*recipient) { + return Err(AccountKeychainError::invalid_call_scope().into()); + } + } } } @@ -1665,6 +1696,59 @@ mod tests { } } + #[test] + fn test_t11_sorted_scope_validation_rejects_duplicates() -> eyre::Result<()> { + let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T11); + StorageCtx::enter(&mut storage, || -> eyre::Result<()> { + let keychain = AccountKeychain::new(); + let target = Address::repeat_byte(0x11); + let recipient = Address::repeat_byte(0x22); + + let error = keychain + .validate_call_scopes(&[ + CallScope { + target, + selectorRules: vec![], + }, + CallScope { + target, + selectorRules: vec![], + }, + ]) + .expect_err("duplicate targets must be rejected"); + assert_invalid_call_scope(error); + + let mut selector_rules = (0u32..4_096) + .map(|selector| SelectorRule { + selector: selector.to_be_bytes().into(), + recipients: vec![], + }) + .collect::>(); + selector_rules.push(selector_rules[0].clone()); + + let error = keychain + .validate_call_scopes(&[CallScope { + target, + selectorRules: selector_rules, + }]) + .expect_err("a duplicate selector at the tail must be rejected"); + assert_invalid_call_scope(error); + + let error = keychain + .validate_call_scopes(&[CallScope { + target: DEFAULT_FEE_TOKEN, + selectorRules: vec![SelectorRule { + selector: TIP20_TRANSFER_SELECTOR.into(), + recipients: vec![recipient, recipient], + }], + }]) + .expect_err("duplicate recipients must be rejected"); + assert_invalid_call_scope(error); + + Ok(()) + }) + } + fn unrestricted_restrictions() -> KeyRestrictions { tempo_alloy::provider::keychain::KeyRestrictions::default().into() } From c8b869a4e2ca64252479cdcdf4caecc8520c0072 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Tue, 25 Aug 2026 16:38:10 +0800 Subject: [PATCH 2/2] perf(keychain): sort recipient keys across forks --- .../precompiles/src/account_keychain/mod.rs | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/crates/precompiles/src/account_keychain/mod.rs b/crates/precompiles/src/account_keychain/mod.rs index 2fc0727bde9..2a0d173d7a8 100644 --- a/crates/precompiles/src/account_keychain/mod.rs +++ b/crates/precompiles/src/account_keychain/mod.rs @@ -1061,14 +1061,14 @@ impl AccountKeychain { } }; - let use_sorted_duplicates = self.storage.spec().is_t11(); - if use_sorted_duplicates && has_duplicates_sorted(rules.iter().map(|rule| rule.selector)) { + let sort_selectors = self.storage.spec().is_t11(); + if sort_selectors && has_duplicates_sorted(rules.iter().map(|rule| rule.selector)) { return Err(AccountKeychainError::invalid_call_scope().into()); } let mut selectors = HashSet::new(); for rule in rules { - if !use_sorted_duplicates && !selectors.insert(rule.selector) { + if !sort_selectors && !selectors.insert(rule.selector) { return Err(AccountKeychainError::invalid_call_scope().into()); } @@ -1080,19 +1080,10 @@ impl AccountKeychain { return Err(AccountKeychainError::invalid_call_scope().into()); } - if use_sorted_duplicates { - if rule.recipients.iter().any(|recipient| recipient.is_zero()) - || has_duplicates_sorted(rule.recipients.iter().copied()) - { - return Err(AccountKeychainError::invalid_call_scope().into()); - } - } else { - let mut unique_recipients = HashSet::new(); - for recipient in &rule.recipients { - if recipient.is_zero() || !unique_recipients.insert(*recipient) { - return Err(AccountKeychainError::invalid_call_scope().into()); - } - } + if rule.recipients.iter().any(|recipient| recipient.is_zero()) + || has_duplicates_sorted(rule.recipients.iter().copied()) + { + return Err(AccountKeychainError::invalid_call_scope().into()); } }