From 49bd99d5e32782ec01639b7cfcd74c2357aa820a Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov <62447812+klkvr@users.noreply.github.com> Date: Fri, 21 Aug 2026 10:16:20 +0000 Subject: [PATCH 1/6] feat(precompiles): implement TIP-1099 keychain ABI changes Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com> --- Cargo.lock | 1 + crates/alloy/src/provider/keychain.rs | 25 +- .../src/precompiles/account_keychain.rs | 6 + crates/precompiles/Cargo.toml | 1 + .../src/account_keychain/dispatch.rs | 225 +++++++++++++++++- .../precompiles/src/account_keychain/mod.rs | 48 +++- 6 files changed, 284 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d4c5454643..f8e0ad024a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13865,6 +13865,7 @@ dependencies = [ "alloy-evm", "alloy-json-abi", "alloy-primitives", + "alloy-rlp", "alloy-signer", "alloy-signer-local", "bitflags 2.11.1", diff --git a/crates/alloy/src/provider/keychain.rs b/crates/alloy/src/provider/keychain.rs index 84460a3ad0..d4bdf618b3 100644 --- a/crates/alloy/src/provider/keychain.rs +++ b/crates/alloy/src/provider/keychain.rs @@ -1,15 +1,16 @@ use core::fmt; use alloy_primitives::{Address, B256, Bytes, TxKind, U256}; +use alloy_rlp::Encodable; use alloy_sol_types::SolCall; use tempo_contracts::precompiles::{ ACCOUNT_KEYCHAIN_ADDRESS, IAccountKeychain::{ KeyRestrictions as AbiKeyRestrictions, LegacyTokenLimit as AbiLegacyTokenLimit, - TokenLimit as AbiTokenLimit, removeAllowedCallsCall, revokeKeyCall, setAllowedCallsCall, + TokenLimit as AbiTokenLimit, removeAllowedCallsCall, revokeKeyCall, updateSpendingLimitCall, }, - ITIP20, authorizeAdminKeyCall, authorizeKeyCall, legacyAuthorizeKeyCall, + ITIP20, authorizeAdminKeyCall, authorizeKeyCall, legacyAuthorizeKeyCall, setAllowedCallsCall, }; use tempo_primitives::{ SignatureType, @@ -309,7 +310,7 @@ pub fn authorize_key_legacy( })) } -/// Build an `authorizeKey(address,uint8,KeyRestrictions)` precompile call. +/// Build a pre-T11 `authorizeKey(address,uint8,KeyRestrictions)` precompile call. pub fn authorize_key( key_id: Address, signature_type: SignatureType, @@ -322,7 +323,7 @@ pub fn authorize_key( }) } -/// Build an `authorizeAdminKey(address,uint8,bytes32)` precompile call. +/// Build a pre-T11 `authorizeAdminKey(address,uint8,bytes32)` precompile call. /// /// Admin keys (TIP-1049) can perform account-management calls such as authorizing or /// revoking other keys and setting a receive policy. Pass [`B256::ZERO`] for `witness` @@ -349,7 +350,7 @@ pub fn update_spending_limit(key_id: Address, token: Address, new_limit: U256) - }) } -/// Build a `setAllowedCalls(address,CallScope[])` precompile call. +/// Build a `setAllowedCalls(address,bytes)` precompile call with RLP-encoded scopes. /// /// # Examples /// @@ -368,9 +369,11 @@ pub fn update_spending_limit(key_id: Address, token: Address, new_limit: U256) - /// let call = set_allowed_calls(key_id, vec![scope]); /// ``` pub fn set_allowed_calls(key_id: Address, scopes: Vec) -> Call { + let mut encoded = Vec::new(); + scopes.encode(&mut encoded); account_keychain_call(setAllowedCallsCall { keyId: key_id, - scopes: scopes.into_iter().map(Into::into).collect(), + scopes: encoded.into(), }) } @@ -407,10 +410,11 @@ fn account_keychain_call(call: impl SolCall) -> Call { mod tests { use super::*; use alloy_primitives::{address, uint}; + use alloy_rlp::Decodable; use tempo_contracts::precompiles::IAccountKeychain::{ CallScope as AbiCallScope, SelectorRule as AbiSelectorRule, SignatureType as AbiSignatureType, removeAllowedCallsCall, revokeKeyCall, - setAllowedCallsCall, updateSpendingLimitCall, + updateSpendingLimitCall, }; #[test] @@ -604,8 +608,11 @@ mod tests { let decoded = setAllowedCallsCall::abi_decode(&call.input).expect("decode setAllowedCalls"); assert_eq!(decoded.keyId, key_id); - assert_eq!(decoded.scopes.len(), 1); - assert_eq!(decoded.scopes[0].selectorRules.len(), 1); + let mut encoded = decoded.scopes.as_ref(); + let decoded_scopes = Vec::::decode(&mut encoded).expect("decode RLP scopes"); + assert!(encoded.is_empty()); + assert_eq!(decoded_scopes.len(), 1); + assert_eq!(decoded_scopes[0].selector_rules.len(), 1); } #[test] diff --git a/crates/contracts/src/precompiles/account_keychain.rs b/crates/contracts/src/precompiles/account_keychain.rs index 7142c683ee..2cd3bb7721 100644 --- a/crates/contracts/src/precompiles/account_keychain.rs +++ b/crates/contracts/src/precompiles/account_keychain.rs @@ -6,6 +6,8 @@ pub use IAccountKeychain::{ authorizeKey_1Call as authorizeKeyCall, authorizeKey_2Call as authorizeKeyWithWitnessCall, getAllowedCallsReturn, getRemainingLimitWithPeriodCall, getRemainingLimitWithPeriodReturn as getRemainingLimitReturn, + setAllowedCalls_0Call as legacySetAllowedCallsCall, + setAllowedCalls_1Call as setAllowedCallsCall, }; crate::sol! { @@ -166,6 +168,10 @@ crate::sol! { CallScope[] calldata scopes ) external; + /// Set or replace RLP-encoded allowed calls for one or more key+target pairs. + /// @dev `scopes` is the canonical RLP encoding of a non-empty CallScope list. + function setAllowedCalls(address keyId, bytes calldata scopes) external; + /// Remove any configured call scope for a key+target pair. function removeAllowedCalls(address keyId, address target) external; diff --git a/crates/precompiles/Cargo.toml b/crates/precompiles/Cargo.toml index 250d00d90e..1dede8f856 100644 --- a/crates/precompiles/Cargo.toml +++ b/crates/precompiles/Cargo.toml @@ -20,6 +20,7 @@ workspace = true tempo-contracts.workspace = true tempo-chainspec = { workspace = true, features = ["evm"] } tempo-primitives = { workspace = true, features = ["evm"] } +alloy-rlp.workspace = true tempo-precompiles-macros.workspace = true alloy = { workspace = true, features = ["sol-types", "consensus"] } alloy-json-abi = { workspace = true, optional = true } diff --git a/crates/precompiles/src/account_keychain/dispatch.rs b/crates/precompiles/src/account_keychain/dispatch.rs index 390059ddcf..96df75d005 100644 --- a/crates/precompiles/src/account_keychain/dispatch.rs +++ b/crates/precompiles/src/account_keychain/dispatch.rs @@ -16,6 +16,7 @@ impl Precompile for AccountKeychain { calldata, |call| match call { IAccountKeychain::IAccountKeychainCalls { + #[schedule(until = T11)] authorizeKey_0(call) => { if self.storage.spec().is_t3() { return self.storage.error_result( @@ -49,15 +50,15 @@ impl Precompile for AccountKeychain { self.authorize_key(sender, c.keyId, c.signatureType, c.config, None) }) }, - #[schedule(since = T3)] + #[schedule(since = T3, until = T11)] authorizeKey_1(call) => mutate_void(call, msg_sender, |sender, c| { self.authorize_key(sender, c.keyId, c.signatureType, c.config, None) }), - #[schedule(since = T5)] + #[schedule(since = T5, until = T11)] authorizeKey_2(call) => mutate_void(call, msg_sender, |sender, c| { self.authorize_key(sender, c.keyId, c.signatureType, c.config, Some(c.witness)) }), - #[schedule(since = T6)] + #[schedule(since = T6, until = T11)] authorizeAdminKey(call) => mutate_void(call, msg_sender, |sender, c| { self.authorize_admin_key(sender, c.keyId, c.signatureType, Some(c.witness)) }), @@ -69,10 +70,14 @@ impl Precompile for AccountKeychain { updateSpendingLimit(call) => mutate_void(call, msg_sender, |sender, c| { self.update_spending_limit(sender, c) }), - #[schedule(since = T3)] - setAllowedCalls(call) => mutate_void(call, msg_sender, |sender, c| { + #[schedule(since = T3, until = T11)] + setAllowedCalls_0(call) => mutate_void(call, msg_sender, |sender, c| { self.set_allowed_calls(sender, c) }), + #[schedule(since = T11)] + setAllowedCalls_1(call) => mutate_void(call, msg_sender, |sender, c| { + self.set_allowed_calls_rlp(sender, c) + }), #[schedule(since = T3)] removeAllowedCalls(call) => mutate_void(call, msg_sender, |sender, c| { self.remove_allowed_calls(sender, c) @@ -108,9 +113,14 @@ mod tests { primitives::{B256, U256}, sol_types::{SolCall, SolError}, }; + use alloy_rlp::Encodable; use tempo_chainspec::hardfork::TempoHardfork; use tempo_contracts::precompiles::{ IAccountKeychain::IAccountKeychainCalls, UnknownFunctionSelector, legacyAuthorizeKeyCall, + legacySetAllowedCallsCall, setAllowedCallsCall, + }; + use tempo_primitives::transaction::{ + CallScope as RlpCallScope, SelectorRule as RlpSelectorRule, }; #[test] @@ -122,6 +132,7 @@ mod tests { .iter() .copied() .filter(|selector| *selector != getRemainingLimitCall::SELECTOR) + .filter(|selector| *selector != setAllowedCallsCall::SELECTOR) .collect(); let unsupported = check_selector_coverage( @@ -137,6 +148,36 @@ mod tests { }) } + #[test] + fn test_t11_account_keychain_selector_coverage() -> eyre::Result<()> { + let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T11); + StorageCtx::enter(&mut storage, || { + let mut keychain = AccountKeychain::new(); + let disabled = [ + legacyAuthorizeKeyCall::SELECTOR, + authorizeKeyCall::SELECTOR, + IAccountKeychain::authorizeKey_2Call::SELECTOR, + IAccountKeychain::authorizeAdminKeyCall::SELECTOR, + legacySetAllowedCallsCall::SELECTOR, + getRemainingLimitCall::SELECTOR, + ]; + let selectors: Vec<_> = IAccountKeychainCalls::SELECTORS + .iter() + .copied() + .filter(|selector| !disabled.contains(selector)) + .collect(); + + let unsupported = check_selector_coverage( + &mut keychain, + &selectors, + "IAccountKeychain T11", + IAccountKeychainCalls::name_by_selector, + ); + assert_full_coverage([unsupported]); + Ok(()) + }) + } + #[test] fn test_legacy_authorize_key_selector_supported_pre_t3() -> eyre::Result<()> { let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C); @@ -406,4 +447,178 @@ mod tests { Ok(()) }) } + + #[test] + fn test_t11_direct_authorization_selectors_are_disabled() -> eyre::Result<()> { + let account = Address::random(); + let key_id = Address::random(); + let witness = B256::random(); + let config = KeyRestrictions { + expiry: u64::MAX, + enforceLimits: false, + limits: vec![], + allowAnyCalls: true, + allowedCalls: vec![], + }; + let calls = [ + legacyAuthorizeKeyCall { + keyId: key_id, + signatureType: IAccountKeychain::SignatureType::Secp256k1, + expiry: u64::MAX, + enforceLimits: false, + limits: vec![], + } + .abi_encode(), + authorizeKeyCall { + keyId: key_id, + signatureType: IAccountKeychain::SignatureType::Secp256k1, + config: config.clone(), + } + .abi_encode(), + IAccountKeychain::authorizeKey_2Call { + keyId: key_id, + signatureType: IAccountKeychain::SignatureType::Secp256k1, + config, + witness, + } + .abi_encode(), + IAccountKeychain::authorizeAdminKeyCall { + keyId: key_id, + signatureType: IAccountKeychain::SignatureType::Secp256k1, + witness, + } + .abi_encode(), + ]; + + let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T11); + StorageCtx::enter(&mut storage, || { + let mut keychain = AccountKeychain::new(); + keychain.initialize()?; + + for calldata in calls { + let expected: [u8; 4] = calldata[..4].try_into().expect("selector"); + let result = keychain.call(&calldata, account)?; + let decoded = UnknownFunctionSelector::abi_decode(&result.bytes)?; + assert_eq!(decoded.selector, expected); + } + assert_eq!(keychain.keys[account][key_id].read()?.expiry, 0); + + Ok(()) + }) + } + + #[test] + fn test_t11_set_allowed_calls_uses_rlp() -> eyre::Result<()> { + let account = Address::random(); + let key_id = Address::random(); + let target = Address::random(); + let selector = [0xaa, 0xbb, 0xcc, 0xdd]; + + let scopes = vec![RlpCallScope { + target, + selector_rules: vec![RlpSelectorRule { + selector, + recipients: vec![], + }], + }]; + let mut encoded_scopes = Vec::new(); + scopes.encode(&mut encoded_scopes); + + let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T11); + StorageCtx::enter(&mut storage, || { + let mut keychain = AccountKeychain::new(); + keychain.initialize()?; + keychain.set_transaction_key(Address::ZERO)?; + keychain.set_tx_origin(account)?; + keychain.authorize_key( + account, + key_id, + IAccountKeychain::SignatureType::Secp256k1, + KeyRestrictions { + expiry: u64::MAX, + enforceLimits: false, + limits: vec![], + allowAnyCalls: true, + allowedCalls: vec![], + }, + None, + )?; + + let old_calldata = legacySetAllowedCallsCall { + keyId: key_id, + scopes: vec![], + } + .abi_encode(); + let old_result = keychain.call(&old_calldata, account)?; + let old_error = UnknownFunctionSelector::abi_decode(&old_result.bytes)?; + assert_eq!( + old_error.selector.as_slice(), + &legacySetAllowedCallsCall::SELECTOR + ); + + let calldata = setAllowedCallsCall { + keyId: key_id, + scopes: encoded_scopes.into(), + } + .abi_encode(); + let result = keychain.call(&calldata, account)?; + assert!(!result.is_revert()); + + let stored = keychain.get_allowed_calls(IAccountKeychain::getAllowedCallsCall { + account, + keyId: key_id, + })?; + assert!(stored.isScoped); + assert_eq!(stored.scopes.len(), 1); + assert_eq!(stored.scopes[0].target, target); + assert_eq!(stored.scopes[0].selectorRules[0].selector, selector); + + Ok(()) + }) + } + + #[test] + fn test_rlp_set_allowed_calls_selector_is_disabled_pre_t11() -> eyre::Result<()> { + let calldata = setAllowedCallsCall { + keyId: Address::random(), + scopes: vec![0xc0].into(), + } + .abi_encode(); + + let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T10); + StorageCtx::enter(&mut storage, || { + let mut keychain = AccountKeychain::new(); + let result = keychain.call(&calldata, Address::random())?; + let decoded = UnknownFunctionSelector::abi_decode(&result.bytes)?; + assert_eq!(decoded.selector.as_slice(), &setAllowedCallsCall::SELECTOR); + Ok(()) + }) + } + + #[test] + fn test_t11_set_allowed_calls_rejects_invalid_rlp() -> eyre::Result<()> { + let key_id = Address::random(); + let mut valid_with_trailing = Vec::new(); + vec![RlpCallScope { + target: Address::random(), + selector_rules: vec![], + }] + .encode(&mut valid_with_trailing); + valid_with_trailing.push(0x80); + + let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T11); + StorageCtx::enter(&mut storage, || { + let mut keychain = AccountKeychain::new(); + for scopes in [vec![], vec![0xc0], vec![0x80], valid_with_trailing] { + let calldata = setAllowedCallsCall { + keyId: key_id, + scopes: scopes.into(), + } + .abi_encode(); + let result = keychain.call(&calldata, Address::random())?; + IAccountKeychain::InvalidCallScope::abi_decode(&result.bytes)?; + } + Ok(()) + }) + } } diff --git a/crates/precompiles/src/account_keychain/mod.rs b/crates/precompiles/src/account_keychain/mod.rs index 2a0d173d7a..1d1e04d508 100644 --- a/crates/precompiles/src/account_keychain/mod.rs +++ b/crates/precompiles/src/account_keychain/mod.rs @@ -11,6 +11,7 @@ pub mod dispatch; use std::collections::HashSet; use alloy::sol_types::SolCall; +use alloy_rlp::Decodable; use tempo_contracts::precompiles::{AccountKeychainError, AccountKeychainEvent, ITIP20}; pub use tempo_contracts::precompiles::{ IAccountKeychain, @@ -19,11 +20,12 @@ pub use tempo_contracts::precompiles::{ burnKeyAuthorizationWitnessCall, getAllowedCallsCall, getKeyCall, getRemainingLimitCall, getRemainingLimitWithPeriodCall, getTransactionKeyCall, isKeyAuthorizationWitnessBurnedCall, removeAllowedCallsCall, revokeKeyCall, - setAllowedCallsCall, updateSpendingLimitCall, + updateSpendingLimitCall, }, authorizeKeyCall, authorizeKeyWithWitnessCall, getAllowedCallsReturn, getRemainingLimitReturn, + legacySetAllowedCallsCall, setAllowedCallsCall, }; -use tempo_primitives::TempoAddressExt; +use tempo_primitives::{TempoAddressExt, transaction::CallScope as RlpCallScope}; use crate::{ ACCOUNT_KEYCHAIN_ADDRESS, @@ -568,25 +570,43 @@ impl AccountKeychain { } /// Root/admin-only create-or-replace updates for one or more target call scopes. - pub fn set_allowed_calls( + pub fn set_allowed_calls_rlp( &mut self, msg_sender: Address, call: setAllowedCallsCall, ) -> Result<()> { - if !self.storage.spec().is_t3() { + let mut encoded_scopes = call.scopes.as_ref(); + let scopes = Vec::::decode(&mut encoded_scopes) + .map_err(|_| AccountKeychainError::invalid_call_scope())?; + if !encoded_scopes.is_empty() { + return Err(AccountKeychainError::invalid_call_scope().into()); + } + let scopes: Vec = scopes.into_iter().map(Into::into).collect(); + if scopes.is_empty() { return Err(AccountKeychainError::invalid_call_scope().into()); } + self.set_allowed_calls_decoded(msg_sender, call.keyId, scopes) + } + + fn set_allowed_calls_decoded( + &mut self, + msg_sender: Address, + key_id: Address, + scopes: Vec, + ) -> Result<()> { + if !self.storage.spec().is_t3() { + return Err(AccountKeychainError::invalid_call_scope().into()); + } self.ensure_admin_caller(msg_sender)?; let current_timestamp = self.storage.timestamp().saturating_to::(); - let key = self.load_active_key(msg_sender, call.keyId, current_timestamp)?; + let key = self.load_active_key(msg_sender, key_id, current_timestamp)?; if key.is_admin { return Err(AccountKeychainError::invalid_key_id().into()); } - let key_hash = Self::spending_limit_key(msg_sender, call.keyId); - let scopes = call.scopes; + let key_hash = Self::spending_limit_key(msg_sender, key_id); if scopes.is_empty() { return Err(AccountKeychainError::invalid_call_scope().into()); @@ -601,6 +621,15 @@ impl AccountKeychain { self.key_scopes[key_hash].is_scoped.write(true) } + /// Pre-T11 ABI-encoded create-or-replace updates for target call scopes. + pub fn set_allowed_calls( + &mut self, + msg_sender: Address, + call: legacySetAllowedCallsCall, + ) -> Result<()> { + self.set_allowed_calls_decoded(msg_sender, call.keyId, call.scopes) + } + /// Root/admin-only removal of one target call scope. pub fn remove_allowed_calls( &mut self, @@ -1594,7 +1623,10 @@ mod tests { use alloy::primitives::{Address, B256, TxKind, U256}; use revm::state::Bytecode; use tempo_chainspec::hardfork::TempoHardfork; - use tempo_contracts::precompiles::{DEFAULT_FEE_TOKEN, IAccountKeychain::SignatureType}; + use tempo_contracts::precompiles::{ + DEFAULT_FEE_TOKEN, IAccountKeychain::SignatureType, + legacySetAllowedCallsCall as setAllowedCallsCall, + }; fn authorize_key( keychain: &mut AccountKeychain, From b2858c66a9df3a424a5ed80868994b159d938bda Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Mon, 24 Aug 2026 01:46:02 +0800 Subject: [PATCH 2/6] feat(precompiles): charge for TIP-1099 RLP input --- .../precompiles/src/account_keychain/mod.rs | 20 ++++++++++ crates/precompiles/src/lib.rs | 37 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/crates/precompiles/src/account_keychain/mod.rs b/crates/precompiles/src/account_keychain/mod.rs index 1d1e04d508..780171e541 100644 --- a/crates/precompiles/src/account_keychain/mod.rs +++ b/crates/precompiles/src/account_keychain/mod.rs @@ -41,6 +41,16 @@ const TIP20_TRANSFER_SELECTOR: [u8; 4] = ITIP20::transferCall::SELECTOR; const TIP20_APPROVE_SELECTOR: [u8; 4] = ITIP20::approveCall::SELECTOR; const TIP20_TRANSFER_WITH_MEMO_SELECTOR: [u8; 4] = ITIP20::transferWithMemoCall::SELECTOR; +/// Additional cost for each 32-byte word decoded as RLP by `setAllowedCalls`. +const RLP_INPUT_PER_WORD_COST: u64 = 50; + +#[inline] +fn rlp_input_cost(input_len: usize) -> u64 { + input_len + .div_ceil(32) + .saturating_mul(RLP_INPUT_PER_WORD_COST as usize) as u64 +} + /// (T7+) Alias for zero remaining periodic spend, used to avoid clearing the storage slot. const ZERO_PERIODIC_REMAINING_SENTINEL: U256 = U256::MAX; @@ -575,6 +585,8 @@ impl AccountKeychain { msg_sender: Address, call: setAllowedCallsCall, ) -> Result<()> { + self.storage.deduct_gas(rlp_input_cost(call.scopes.len()))?; + let mut encoded_scopes = call.scopes.as_ref(); let scopes = Vec::::decode(&mut encoded_scopes) .map_err(|_| AccountKeychainError::invalid_call_scope())?; @@ -1628,6 +1640,14 @@ mod tests { legacySetAllowedCallsCall as setAllowedCallsCall, }; + #[test] + fn test_rlp_input_cost() { + assert_eq!(rlp_input_cost(0), 0); + assert_eq!(rlp_input_cost(1), 50); + assert_eq!(rlp_input_cost(32), 50); + assert_eq!(rlp_input_cost(33), 100); + } + fn authorize_key( keychain: &mut AccountKeychain, msg_sender: Address, diff --git a/crates/precompiles/src/lib.rs b/crates/precompiles/src/lib.rs index 9d1c16fbf0..9fdab025f4 100644 --- a/crates/precompiles/src/lib.rs +++ b/crates/precompiles/src/lib.rs @@ -416,6 +416,7 @@ where mod tests { use super::*; use crate::{ + account_keychain::setAllowedCallsCall, storage::{StorageCtx, hashmap::HashMapStorageProvider}, tip20::TIP20Token, }; @@ -1022,6 +1023,42 @@ mod tests { assert_eq!(input_cost(TempoHardfork::T11, 33).unwrap(), 60); } + #[test] + fn test_t11_set_allowed_calls_charges_rlp_input_before_decoding() { + let mut cfg = CfgEnv::::default(); + cfg.set_spec_and_mainnet_gas_params(TempoHardfork::T11); + let precompile = + tempo_precompile!("AccountKeychain", &cfg, |_input| { AccountKeychain::new() }); + + let calldata: Bytes = setAllowedCallsCall { + keyId: Address::random(), + scopes: vec![0xc0].into(), + } + .abi_encode() + .into(); + let base_cost = input_cost(TempoHardfork::T11, calldata.len()); + + let db = CacheDB::new(EmptyDB::new()); + let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default()); + let block = evm.block.clone(); + let tx = TxEnv::default(); + let evm_internals = EvmInternals::new(evm.journal_mut(), &block, &cfg, &tx); + let input = PrecompileInput { + data: &calldata, + caller: Address::ZERO, + internals: evm_internals, + gas: base_cost + 49, + is_static: false, + value: U256::ZERO, + target_address: ACCOUNT_KEYCHAIN_ADDRESS, + bytecode_address: ACCOUNT_KEYCHAIN_ADDRESS, + reservoir: 0, + }; + + let output = AlloyEvmPrecompile::call(&precompile, input).expect("expected OOG output"); + assert!(output.is_halt()); + } + #[test] fn test_extend_tempo_precompiles_registers_precompiles() { let mut cfg = CfgEnv::::default(); From b2c0219c95d941d06d75773e104f1ba9a0bfa5ba Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Mon, 24 Aug 2026 01:55:26 +0800 Subject: [PATCH 3/6] test(precompiles): assert RLP charge OOG reason --- crates/precompiles/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/precompiles/src/lib.rs b/crates/precompiles/src/lib.rs index 9fdab025f4..b14e732ce0 100644 --- a/crates/precompiles/src/lib.rs +++ b/crates/precompiles/src/lib.rs @@ -431,6 +431,7 @@ mod tests { use revm::{ context::{ContextTr, TxEnv}, database::{CacheDB, EmptyDB}, + precompile::{PrecompileHalt, PrecompileStatus}, state::{AccountInfo, Bytecode}, }; use tempo_contracts::precompiles::{ITIP20, UnknownFunctionSelector}; @@ -1056,7 +1057,10 @@ mod tests { }; let output = AlloyEvmPrecompile::call(&precompile, input).expect("expected OOG output"); - assert!(output.is_halt()); + assert!(matches!( + output.status, + PrecompileStatus::Halt(PrecompileHalt::OutOfGas) + )); } #[test] From 1b5ee58e36991abefbb59f2abc98a7e6addc4bc5 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Mon, 24 Aug 2026 16:04:50 +0800 Subject: [PATCH 4/6] test(precompiles): update T11 compatibility coverage --- crates/node/tests/it/storage_credits.rs | 13 ++++++++++--- crates/node/tests/it/tempo_transaction/local.rs | 10 ++++++++-- tips/verify/foundry.toml | 3 +++ tips/verify/lib/tempo-std | 2 +- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/crates/node/tests/it/storage_credits.rs b/crates/node/tests/it/storage_credits.rs index f153c5c549..9fc2717011 100644 --- a/crates/node/tests/it/storage_credits.rs +++ b/crates/node/tests/it/storage_credits.rs @@ -1,4 +1,4 @@ -use crate::utils::{TEST_MNEMONIC, TestNodeBuilder, setup_test_token}; +use crate::utils::{ForkSchedule, TEST_MNEMONIC, TestNodeBuilder, setup_test_token}; use alloy::{ network::ReceiptResponse, primitives::{Address, B256, Bytes, U256, aliases::U96}, @@ -12,6 +12,7 @@ use alloy::{ use alloy_eips::{BlockId, Encodable2718}; use alloy_rpc_types_eth::{TransactionReceipt, TransactionRequest}; use tempo_alloy::rpc::TempoTransactionReceipt; +use tempo_chainspec::hardfork::TempoHardfork; use tempo_contracts::precompiles::{ DEFAULT_FEE_TOKEN, IFeeManager, IReceivePolicyGuard, IStorageCredits, ITIP20, ITIP20ChannelReserve, ITIP403Registry, ITIPFeeAMM, @@ -106,7 +107,10 @@ async fn send_tempo_tx( async fn test_tip1060_keychain_fee_refund_does_not_retain_storage_credit() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let setup = TestNodeBuilder::new().build_http_only().await?; + let setup = TestNodeBuilder::new() + .with_schedule(ForkSchedule::DevnetAt(TempoHardfork::T10)) + .build_http_only() + .await?; let root = MnemonicBuilder::from_phrase(TEST_MNEMONIC).build()?; let root_addr = root.address(); let provider = ProviderBuilder::new() @@ -1113,7 +1117,10 @@ async fn test_tip1060_successful_keychain_spend_fee_refund_cancels_restored_limi -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let setup = TestNodeBuilder::new().build_http_only().await?; + let setup = TestNodeBuilder::new() + .with_schedule(ForkSchedule::DevnetAt(TempoHardfork::T10)) + .build_http_only() + .await?; let root = MnemonicBuilder::from_phrase(TEST_MNEMONIC).build()?; let root_addr = root.address(); let provider = ProviderBuilder::new() diff --git a/crates/node/tests/it/tempo_transaction/local.rs b/crates/node/tests/it/tempo_transaction/local.rs index 42c84eb319..ff45aca2f3 100644 --- a/crates/node/tests/it/tempo_transaction/local.rs +++ b/crates/node/tests/it/tempo_transaction/local.rs @@ -1532,7 +1532,10 @@ async fn test_key_authorization_witness_burn_evicts_pending_replay() -> eyre::Re async fn test_t6_authorize_admin_key_abi_e2e() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let mut setup = TestNodeBuilder::new().build_with_node_access().await?; + let mut setup = TestNodeBuilder::new() + .with_schedule(ForkSchedule::DevnetAt(TempoHardfork::T10)) + .build_with_node_access() + .await?; let root_signer = MnemonicBuilder::from_phrase(TEST_MNEMONIC).build()?; let root_addr = root_signer.address(); let provider = ProviderBuilder::new_with_network::() @@ -1609,7 +1612,10 @@ async fn test_t6_inline_admin_key_authorization_e2e() -> eyre::Result<()> { async fn test_t6_admin_key_authorizes_child_admin_key_e2e() -> eyre::Result<()> { reth_tracing::init_test_tracing(); - let mut setup = TestNodeBuilder::new().build_with_node_access().await?; + let mut setup = TestNodeBuilder::new() + .with_schedule(ForkSchedule::DevnetAt(TempoHardfork::T10)) + .build_with_node_access() + .await?; let root_signer = MnemonicBuilder::from_phrase(TEST_MNEMONIC).build()?; let root_addr = root_signer.address(); let provider = ProviderBuilder::new_with_network::() diff --git a/tips/verify/foundry.toml b/tips/verify/foundry.toml index 0a0cc6036a..d14676121f 100644 --- a/tips/verify/foundry.toml +++ b/tips/verify/foundry.toml @@ -20,6 +20,9 @@ invariant = { runs = 10, depth = 50, fail_on_revert = true, show_solidity = true [profile.next] hardfork = "tempo:T11" +# These suites exercise the direct authorization selectors removed in T11. +# They continue to run under the default T10 profile. +no_match_contract = "^AccountKeychain(Test|InvariantTest)$" [profile.fuzz500] invariant = { runs = 500, depth = 500, fail_on_revert = true, show_solidity = true } diff --git a/tips/verify/lib/tempo-std b/tips/verify/lib/tempo-std index 96f882963c..a1876f59fb 160000 --- a/tips/verify/lib/tempo-std +++ b/tips/verify/lib/tempo-std @@ -1 +1 @@ -Subproject commit 96f882963ce3b0e0abc7ec2b41d03d42457877bb +Subproject commit a1876f59fb3d5002496bacd9635b175a2349b40f From dea4a5f742db5c759f78968113685c0cec76a196 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Fri, 28 Aug 2026 03:24:44 +0800 Subject: [PATCH 5/6] decode_exact --- crates/precompiles/src/account_keychain/mod.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/precompiles/src/account_keychain/mod.rs b/crates/precompiles/src/account_keychain/mod.rs index 780171e541..7595638d4d 100644 --- a/crates/precompiles/src/account_keychain/mod.rs +++ b/crates/precompiles/src/account_keychain/mod.rs @@ -11,7 +11,6 @@ pub mod dispatch; use std::collections::HashSet; use alloy::sol_types::SolCall; -use alloy_rlp::Decodable; use tempo_contracts::precompiles::{AccountKeychainError, AccountKeychainEvent, ITIP20}; pub use tempo_contracts::precompiles::{ IAccountKeychain, @@ -587,12 +586,8 @@ impl AccountKeychain { ) -> Result<()> { self.storage.deduct_gas(rlp_input_cost(call.scopes.len()))?; - let mut encoded_scopes = call.scopes.as_ref(); - let scopes = Vec::::decode(&mut encoded_scopes) + let scopes: Vec = alloy_rlp::decode_exact(call.scopes.as_ref()) .map_err(|_| AccountKeychainError::invalid_call_scope())?; - if !encoded_scopes.is_empty() { - return Err(AccountKeychainError::invalid_call_scope().into()); - } let scopes: Vec = scopes.into_iter().map(Into::into).collect(); if scopes.is_empty() { return Err(AccountKeychainError::invalid_call_scope().into()); From ad7a1bfae891fb5e1324ead46c3c1b5d7fb4ba63 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Fri, 28 Aug 2026 22:23:35 +0800 Subject: [PATCH 6/6] fix(precompiles): unwrap TIP-1099 input cost in test --- crates/precompiles/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/precompiles/src/lib.rs b/crates/precompiles/src/lib.rs index b14e732ce0..d2dd1d8f75 100644 --- a/crates/precompiles/src/lib.rs +++ b/crates/precompiles/src/lib.rs @@ -1037,7 +1037,7 @@ mod tests { } .abi_encode() .into(); - let base_cost = input_cost(TempoHardfork::T11, calldata.len()); + let base_cost = input_cost(TempoHardfork::T11, calldata.len()).unwrap(); let db = CacheDB::new(EmptyDB::new()); let mut evm = EthEvmFactory::default().create_evm(db, EvmEnv::default());