diff --git a/crates/precompiles/src/account_keychain/mod.rs b/crates/precompiles/src/account_keychain/mod.rs index 23e3345959..15f4f3f927 100644 --- a/crates/precompiles/src/account_keychain/mod.rs +++ b/crates/precompiles/src/account_keychain/mod.rs @@ -28,7 +28,7 @@ pub use tempo_contracts::precompiles::{ use crate::{ ACCOUNT_KEYCHAIN_ADDRESS, error::Result, - storage::{Handler, Mapping, Set, packing::insert_into_word}, + storage::{EnumerableMap, Handler, Mapping, Set, packing::insert_into_word}, tip20_factory::TIP20Factory, }; use alloy::primitives::{Address, B256, FixedBytes, TxKind, U256, keccak256}; @@ -85,12 +85,11 @@ pub struct SelectorScope { /// mode: /// - 0 => unset/disabled /// - 1 => all selectors allowed -/// - 2 => only selectors in the set are allowed +/// - 2 => only selectors in the configured list are allowed #[derive(Debug, Clone, Storable, Default)] pub struct TargetScope { pub mode: u8, - pub selectors: Set>, - pub selector_scopes: Mapping, SelectorScope>, + pub selectors: EnumerableMap, SelectorScope>, } /// Key-level call scope. @@ -102,8 +101,7 @@ pub struct TargetScope { #[derive(Debug, Clone, Storable, Default)] pub struct KeyScope { pub mode: u8, - pub targets: Set
, - pub target_scopes: Mapping, + pub targets: EnumerableMap, } /// Key information stored in the precompile @@ -617,7 +615,7 @@ impl AccountKeychain { }]); } - let targets = self.key_scopes[key_hash].targets.read()?; + let targets = self.key_scopes[key_hash].targets.keys()?; if targets.is_empty() { return Ok(vec![CallScope { target: Address::ZERO, @@ -628,9 +626,7 @@ impl AccountKeychain { let mut scopes = Vec::new(); for target in targets { - let target_mode = self.key_scopes[key_hash].target_scopes[target] - .mode - .read()?; + let target_mode = self.key_scopes[key_hash].targets[target].mode.read()?; let scope = match target_mode { 1 => CallScope { @@ -640,22 +636,19 @@ impl AccountKeychain { }, 2 => { let mut rules = Vec::new(); - let selectors = self.key_scopes[key_hash].target_scopes[target] - .selectors - .read()?; + let selectors = self.key_scopes[key_hash].targets[target].selectors.keys()?; for selector in selectors { - let selector_mode = self.key_scopes[key_hash].target_scopes[target] - .selector_scopes[selector] + let selector_mode = self.key_scopes[key_hash].targets[target].selectors + [selector] .mode .read()?; let recipients = if selector_mode == 2 { - let recipients: Vec
= self.key_scopes[key_hash].target_scopes - [target] - .selector_scopes[selector] - .recipients - .read()? - .into(); + let recipients: Vec
= + self.key_scopes[key_hash].targets[target].selectors[selector] + .recipients + .read()? + .into(); recipients } else if selector_mode == 1 { Vec::new() @@ -785,9 +778,7 @@ impl AccountKeychain { TxKind::Create => return Err(AccountKeychainError::call_not_allowed().into()), }; - let target_mode = self.key_scopes[key_hash].target_scopes[target] - .mode - .read()?; + let target_mode = self.key_scopes[key_hash].targets[target].mode.read()?; if target_mode == 1 { return Ok(()); } @@ -800,16 +791,8 @@ impl AccountKeychain { return Err(AccountKeychainError::call_not_allowed().into()); } - let selector = FixedBytes::<4>::from([input[0], input[1], input[2], input[3]]); - if !self.key_scopes[key_hash].target_scopes[target] - .selectors - .contains(&selector)? - { - return Err(AccountKeychainError::call_not_allowed().into()); - } - - let selector_mode = self.key_scopes[key_hash].target_scopes[target].selector_scopes - [selector] + let selector: FixedBytes<4> = [input[0], input[1], input[2], input[3]].into(); + let selector_mode = self.key_scopes[key_hash].targets[target].selectors[selector] .mode .read()?; if selector_mode == 1 { @@ -829,7 +812,7 @@ impl AccountKeychain { } let recipient = Address::from_slice(&recipient_word[12..]); - if self.key_scopes[key_hash].target_scopes[target].selector_scopes[selector] + if self.key_scopes[key_hash].targets[target].selectors[selector] .recipients .contains(&recipient)? { @@ -846,8 +829,9 @@ impl AccountKeychain { ) -> Result<()> { // Fresh authorizations should not have any pre-existing call-scope rows because // `authorize_key` rejects both existing and previously revoked keys before reaching this - // path. We still clear the scope tree first as a defense-in-depth measure against stale or - // out-of-band state, and keep it because the valid-path cost is low (empty target set). + // path. We still clear the indexed scope tree first as a defense-in-depth measure against + // stale state. Mapping rows are the internal source of truth; vec indexes are maintained + // by mutator paths so externally enumerated scopes remain correct. self.clear_all_target_scopes(account_key)?; match allowed_calls { @@ -888,7 +872,7 @@ impl AccountKeychain { } fn clear_all_target_scopes(&mut self, account_key: B256) -> Result<()> { - let targets = self.key_scopes[account_key].targets.read()?; + let targets = self.key_scopes[account_key].targets.keys()?; for target in targets { self.remove_target_scope(account_key, target)?; } @@ -897,32 +881,32 @@ impl AccountKeychain { } fn remove_target_scope(&mut self, account_key: B256, target: Address) -> Result<()> { - if !self.key_scopes[account_key].targets.remove(&target)? { + if self.key_scopes[account_key].targets[target].mode.read()? == 0 { return Ok(()); } self.clear_target_selectors(account_key, target)?; - self.key_scopes[account_key].target_scopes[target] - .mode - .write(0) + self.key_scopes[account_key].targets[target].mode.write(0)?; + self.key_scopes[account_key].targets.remove_key(&target)?; + Ok(()) } fn clear_target_selectors(&mut self, account_key: B256, target: Address) -> Result<()> { - let selectors = self.key_scopes[account_key].target_scopes[target] + let selectors = self.key_scopes[account_key].targets[target] .selectors - .read()?; + .keys()?; for selector in selectors { - self.key_scopes[account_key].target_scopes[target].selector_scopes[selector] + self.key_scopes[account_key].targets[target].selectors[selector] .recipients .delete()?; - self.key_scopes[account_key].target_scopes[target].selector_scopes[selector] + self.key_scopes[account_key].targets[target].selectors[selector] .mode .write(0)?; } - self.key_scopes[account_key].target_scopes[target] + self.key_scopes[account_key].targets[target] .selectors - .delete() + .clear_keys() } fn upsert_target_scope( @@ -938,52 +922,49 @@ impl AccountKeychain { self.validate_selector_rules(target, rules)?; } - if !self.key_scopes[account_key].targets.contains(&target)? { + if !self.key_scopes[account_key] + .targets + .contains_mapped(&target, |scope| scope.mode.read().map(|mode| mode != 0))? + { let count = self.key_scopes[account_key].targets.len()?; if count >= MAX_CALL_SCOPES as usize { return Err(AccountKeychainError::scope_limit_exceeded().into()); } - self.key_scopes[account_key].targets.insert(target)?; + self.key_scopes[account_key] + .targets + .insert_key_unchecked(target)?; } self.clear_target_selectors(account_key, target)?; match selector_rules { None => { - self.key_scopes[account_key].target_scopes[target] - .mode - .write(1)?; + self.key_scopes[account_key].targets[target].mode.write(1)?; } Some(rules) => { - self.key_scopes[account_key].target_scopes[target] - .mode - .write(2)?; + self.key_scopes[account_key].targets[target].mode.write(2)?; for rule in rules { - let selector = FixedBytes::<4>::from(rule.selector); - self.key_scopes[account_key].target_scopes[target] + let selector: FixedBytes<4> = rule.selector.into(); + self.key_scopes[account_key].targets[target] .selectors - .insert(selector)?; + .insert_key_unchecked(selector)?; match rule.recipients { None => { - self.key_scopes[account_key].target_scopes[target].selector_scopes - [selector] + self.key_scopes[account_key].targets[target].selectors[selector] .mode .write(1)?; - self.key_scopes[account_key].target_scopes[target].selector_scopes - [selector] + self.key_scopes[account_key].targets[target].selectors[selector] .recipients .delete()?; } Some(recipients) => { - self.key_scopes[account_key].target_scopes[target].selector_scopes - [selector] + self.key_scopes[account_key].targets[target].selectors[selector] .mode .write(2)?; - self.key_scopes[account_key].target_scopes[target].selector_scopes - [selector] + self.key_scopes[account_key].targets[target].selectors[selector] .recipients .write(Set::from(recipients))?; } @@ -3551,6 +3532,69 @@ mod tests { }) } + #[test] + fn test_t3_set_allowed_calls_replaces_selector_index_without_duplicates() -> eyre::Result<()> { + let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3); + let account = Address::random(); + let key_id = Address::random(); + let target = DEFAULT_FEE_TOKEN; + let first_selector = TIP20_TRANSFER_SELECTOR; + let second_selector = TIP20_APPROVE_SELECTOR; + + 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, + authorizeKeyCall { + keyId: key_id, + signatureType: SignatureType::Secp256k1, + config: KeyRestrictions { + expiry: u64::MAX, + enforceLimits: false, + limits: vec![], + enforceAllowedCalls: false, + allowedCalls: vec![], + }, + }, + )?; + + let set_scope = |keychain: &mut AccountKeychain, selector: [u8; 4]| { + keychain.set_allowed_calls( + account, + setAllowedCallsCall { + keyId: key_id, + scope: CallScope { + target, + allowAllSelectors: false, + selectorRules: vec![SelectorRule { + selector: selector.into(), + recipients: vec![], + }], + }, + }, + ) + }; + + set_scope(&mut keychain, first_selector)?; + set_scope(&mut keychain, second_selector)?; + + let scopes = keychain.get_allowed_calls(getAllowedCallsCall { + account, + keyId: key_id, + })?; + assert_eq!(scopes.len(), 1); + assert_eq!(scopes[0].target, target); + assert_eq!(scopes[0].selectorRules.len(), 1); + assert_eq!(*scopes[0].selectorRules[0].selector, second_selector); + + Ok(()) + }) + } + #[test] fn test_t3_set_allowed_calls_allow_all_selectors_ignores_selector_rules() -> eyre::Result<()> { let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3); diff --git a/crates/precompiles/src/storage/types/enumerable_map.rs b/crates/precompiles/src/storage/types/enumerable_map.rs new file mode 100644 index 0000000000..f5427ef07f --- /dev/null +++ b/crates/precompiles/src/storage/types/enumerable_map.rs @@ -0,0 +1,408 @@ +//! Minimal enumerable map for EVM storage. +//! +//! This stores an authoritative `Mapping` alongside a `Vec` index used only for +//! enumeration and bounded cleanup. It deliberately does not maintain an auxiliary positions +//! mapping, so insert/remove on the key index are O(n) scans over the key list. +//! +//! # Storage Layout +//! +//! EnumerableMap uses two storage structures: +//! - **Keys Vec**: a `Vec` storing the enumerated keys at `base_slot` +//! - **Values Mapping**: a `Mapping` at `base_slot + 1` +//! +//! # Design +//! +//! - Reads on hot paths should go through the mapping, e.g. `map[key].field.read()?` +//! - The key vector is only for cold-path enumeration and cleanup +//! - Mutations to mapped values and the key index are intentionally separate so callers can +//! decide how liveness is represented in `V` + +use alloy::primitives::{Address, U256}; +use std::{ + fmt, + hash::Hash, + ops::{Index, IndexMut}, +}; + +use crate::{ + error::{Result, TempoPrecompileError}, + storage::{ + Handler, Layout, LayoutCtx, Mapping, Storable, StorableType, StorageKey, vec::VecHandler, + }, +}; + +/// Enumerable map storage primitive backed by `Vec + Mapping`. +pub struct EnumerableMap +where + K: Storable + StorageKey + Hash + Eq + Clone, + V: StorableType, +{ + keys: VecHandler, + values: Mapping, + base_slot: U256, + address: Address, +} + +impl EnumerableMap +where + K: Storable + StorageKey + Hash + Eq + Clone, + V: StorableType, +{ + /// Creates a new enumerable map handler for the given base slot. + #[inline] + pub fn new(base_slot: U256, address: Address) -> Self { + Self { + keys: VecHandler::new(base_slot, address), + values: Mapping::new(base_slot + U256::ONE, address), + base_slot, + address, + } + } + + /// Returns the base storage slot for this map. + #[inline] + pub fn base_slot(&self) -> U256 { + self.base_slot + } + + /// Returns the number of enumerated keys. + #[inline] + pub fn len(&self) -> Result { + self.keys.len() + } + + /// Returns whether the enumerated key index is empty. + #[inline] + pub fn is_empty(&self) -> Result { + self.keys.is_empty() + } + + /// Returns the enumerated keys in insertion order. + #[inline] + pub fn keys(&self) -> Result> { + self.keys.read() + } + + /// Returns true if the key exists in the enumerable index. + #[inline] + pub fn contains_key(&self, key: &K) -> Result { + Ok(self.keys()?.contains(key)) + } + + /// Returns whether the mapped value for `key` should be considered present. + /// + /// This is for value types that encode liveness in the mapped payload itself, + /// e.g. a `mode` field where `0` means absent and non-zero means present. + /// The enumerable key index is not consulted. + #[inline] + pub fn contains_mapped(&self, key: &K, is_present: F) -> Result + where + F: FnOnce(&V::Handler) -> Result, + { + is_present(self.at(key)) + } + + /// Adds a key to the enumerable index if it is not already present. + /// + /// Returns `true` when the key was inserted into the index. + #[inline] + pub fn insert_key(&mut self, key: K) -> Result + where + K::Handler: Handler, + { + if self.contains_key(&key)? { + return Ok(false); + } + + self.keys.push(key)?; + Ok(true) + } + + /// Appends a key to the enumerable index without checking for duplicates. + /// + /// Callers must only use this after proving absence from the authoritative + /// mapping or otherwise guaranteeing uniqueness. + #[inline] + pub fn insert_key_unchecked(&self, key: K) -> Result<()> + where + K::Handler: Handler, + { + self.keys.push(key) + } + + /// Removes a key from the enumerable index. + /// + /// This only updates the key index. Callers remain responsible for clearing any mapped value. + #[inline] + pub fn remove_key(&mut self, key: &K) -> Result { + let mut keys = self.keys()?; + let original_len = keys.len(); + keys.retain(|existing| existing != key); + + if keys.len() == original_len { + return Ok(false); + } + + self.keys.write(keys)?; + Ok(true) + } + + /// Clears the enumerable key index without touching mapped values. + #[inline] + pub fn clear_keys(&mut self) -> Result<()> { + self.keys.delete() + } + + /// Returns the value handler for the given key. + #[inline] + pub fn at(&self, key: &K) -> &V::Handler { + self.values.at(key) + } + + /// Returns the mutable value handler for the given key. + #[inline] + pub fn at_mut(&mut self, key: &K) -> &mut V::Handler { + self.values.at_mut(key) + } +} + +impl Default for EnumerableMap +where + K: Storable + StorageKey + Hash + Eq + Clone, + V: StorableType, +{ + fn default() -> Self { + Self::new(U256::ZERO, Address::ZERO) + } +} + +impl Storable for EnumerableMap +where + K: Storable + StorageKey + Hash + Eq + Clone, + V: StorableType, +{ + fn load( + _storage: &S, + _slot: U256, + _ctx: LayoutCtx, + ) -> Result { + Err(TempoPrecompileError::Fatal( + "EnumerableMap must be accessed through its generated handler".into(), + )) + } + + fn store( + &self, + _storage: &mut S, + _slot: U256, + _ctx: LayoutCtx, + ) -> Result<()> { + Err(TempoPrecompileError::Fatal( + "EnumerableMap must be accessed through its generated handler".into(), + )) + } + + fn delete( + _storage: &mut S, + _slot: U256, + _ctx: LayoutCtx, + ) -> Result<()> { + Err(TempoPrecompileError::Fatal( + "EnumerableMap must be accessed through its generated handler".into(), + )) + } +} + +impl Index for EnumerableMap +where + K: Storable + StorageKey + Hash + Eq + Clone, + V: StorableType, +{ + type Output = V::Handler; + + #[inline] + fn index(&self, key: K) -> &Self::Output { + &self.values[key] + } +} + +impl IndexMut for EnumerableMap +where + K: Storable + StorageKey + Hash + Eq + Clone, + V: StorableType, +{ + #[inline] + fn index_mut(&mut self, key: K) -> &mut Self::Output { + &mut self.values[key] + } +} + +impl StorableType for EnumerableMap +where + K: Storable + StorageKey + Hash + Eq + Clone, + V: StorableType, +{ + const LAYOUT: Layout = Layout::Slots(2); + type Handler = Self; + + fn handle(slot: U256, _ctx: LayoutCtx, address: Address) -> Self::Handler { + Self::new(slot, address) + } +} + +impl fmt::Debug for EnumerableMap +where + K: Storable + StorageKey + Hash + Eq + Clone, + V: StorableType, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("EnumerableMap") + .field("base_slot", &self.base_slot) + .field("address", &self.address) + .finish() + } +} + +impl Clone for EnumerableMap +where + K: Storable + StorageKey + Hash + Eq + Clone, + V: StorableType, +{ + fn clone(&self) -> Self { + Self::new(self.base_slot, self.address) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + storage::{Handler, LayoutCtx, StorableType, StorageCtx}, + test_util::setup_storage, + }; + use tempo_precompiles_macros::Storable; + + #[derive(Debug, Clone, Default, PartialEq, Eq, Storable)] + struct TestScope { + mode: u8, + payload: u64, + } + + #[derive(Debug, Clone, Storable, Default)] + struct TestContainer { + marker: u8, + entries: EnumerableMap, + tail: u8, + } + + #[test] + fn test_enumerable_map_key_index_preserves_order_and_uniqueness() { + let (mut storage, address) = setup_storage(); + StorageCtx::enter(&mut storage, || -> Result<()> { + let mut map = EnumerableMap::::new(U256::ZERO, address); + let first = Address::repeat_byte(0x11); + let second = Address::repeat_byte(0x22); + + assert!(map.insert_key(first)?); + assert!(map.insert_key(second)?); + assert!(!map.insert_key(first)?); + + assert_eq!(map.keys()?, vec![first, second]); + assert!(map.contains_key(&first)?); + assert_eq!(map.len()?, 2); + + Ok(()) + }) + .unwrap(); + } + + #[test] + fn test_enumerable_map_remove_key_only_updates_index() { + let (mut storage, address) = setup_storage(); + StorageCtx::enter(&mut storage, || -> Result<()> { + let mut map = EnumerableMap::::new(U256::ZERO, address); + let key = Address::repeat_byte(0x33); + + assert!(map.insert_key(key)?); + map[key].write(7)?; + + assert!(map.remove_key(&key)?); + assert!(!map.contains_key(&key)?); + assert!(map.keys()?.is_empty()); + assert_eq!(map[key].read()?, 7); + + Ok(()) + }) + .unwrap(); + } + + #[test] + fn test_enumerable_map_contains_mapped_uses_value_liveness() { + let (mut storage, address) = setup_storage(); + StorageCtx::enter(&mut storage, || -> Result<()> { + let key = Address::repeat_byte(0x44); + let mut map = EnumerableMap::::new(U256::ZERO, address); + + assert!(!map.contains_mapped(&key, |value| value.read().map(|mode| mode != 0))?); + + map[key].write(2)?; + assert!(map.contains_mapped(&key, |value| value.read().map(|mode| mode != 0))?); + + map[key].write(0)?; + assert!(!map.contains_mapped(&key, |value| value.read().map(|mode| mode != 0))?); + + Ok(()) + }) + .unwrap(); + } + + #[test] + fn test_enumerable_map_embedded_layout_and_persistence() { + let (mut storage, address) = setup_storage(); + StorageCtx::enter(&mut storage, || -> Result<()> { + let first = Address::repeat_byte(0x55); + let second = Address::repeat_byte(0x66); + + let mut container = TestContainer::handle(U256::ZERO, LayoutCtx::FULL, address); + + assert_eq!(container.marker.slot(), U256::ZERO); + assert_eq!(container.entries.base_slot(), U256::ONE); + assert_eq!(container.tail.slot(), U256::from(3)); + + container.marker.write(9)?; + container.tail.write(7)?; + + container.entries.insert_key_unchecked(first)?; + container.entries[first].mode.write(2)?; + container.entries[first].payload.write(11)?; + + container.entries.insert_key_unchecked(second)?; + container.entries[second].mode.write(1)?; + container.entries[second].payload.write(22)?; + + let container = TestContainer::handle(U256::ZERO, LayoutCtx::FULL, address); + assert_eq!(container.marker.read()?, 9); + assert_eq!(container.tail.read()?, 7); + assert_eq!(container.entries.keys()?, vec![first, second]); + assert!( + container + .entries + .contains_mapped(&first, |scope| { scope.mode.read().map(|mode| mode != 0) })? + ); + assert_eq!(container.entries[first].payload.read()?, 11); + assert_eq!(container.entries[second].payload.read()?, 22); + + let mut container = TestContainer::handle(U256::ZERO, LayoutCtx::FULL, address); + assert!(container.entries.remove_key(&first)?); + + let container = TestContainer::handle(U256::ZERO, LayoutCtx::FULL, address); + assert_eq!(container.entries.keys()?, vec![second]); + assert_eq!(container.entries[first].payload.read()?, 11); + assert_eq!(container.marker.read()?, 9); + assert_eq!(container.tail.read()?, 7); + + Ok(()) + }) + .unwrap(); + } +} diff --git a/crates/precompiles/src/storage/types/mod.rs b/crates/precompiles/src/storage/types/mod.rs index b2bfd75602..b1fb10e3f4 100644 --- a/crates/precompiles/src/storage/types/mod.rs +++ b/crates/precompiles/src/storage/types/mod.rs @@ -11,8 +11,10 @@ pub mod mapping; pub use mapping::*; pub mod array; +pub mod enumerable_map; pub mod set; pub mod vec; +pub use enumerable_map::EnumerableMap; pub use set::{Set, SetHandler}; pub mod bytes_like; diff --git a/crates/primitives/src/transaction/key_authorization.rs b/crates/primitives/src/transaction/key_authorization.rs index b6e6d5c1d3..523ff541bc 100644 --- a/crates/primitives/src/transaction/key_authorization.rs +++ b/crates/primitives/src/transaction/key_authorization.rs @@ -117,6 +117,9 @@ impl Encodable for TokenLimit { /// - `None` => allow any selector for this target /// - `Some([])` => deny all selectors for this target /// - `Some([..])` => allow exactly the listed selector rules +/// +/// On the raw protocol model, `Some([])` is a no-match scope. The Solidity +/// keychain mutator normalizes that form as delete-equivalent for the target. #[derive(Clone, Debug, PartialEq, Eq, Hash, alloy_rlp::RlpEncodable, alloy_rlp::RlpDecodable)] #[rlp(trailing)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -344,11 +347,11 @@ impl KeyAuthorization { // Storage write accounting: // - account mode write: 1 - // - each target insertion + target mode write: 3 + 1 - // - each selector insertion + selector mode write: 3 + 1 + // - each target vec push + target mode write: 2 + 1 + // - each selector vec push + selector mode write: 2 + 1 // - recipient-constrained selectors also write recipient set length: +1 per selector // - recipient set values+positions: +2 per recipient - 1 + scopes.len() as u64 * 4 + selectors * 4 + constrained_selectors + recipients * 2 + 1 + scopes.len() as u64 * 3 + selectors * 3 + constrained_selectors + recipients * 2 } } } diff --git a/crates/revm/src/handler.rs b/crates/revm/src/handler.rs index 83c4a42a29..84ad208117 100644 --- a/crates/revm/src/handler.rs +++ b/crates/revm/src/handler.rs @@ -2667,10 +2667,10 @@ mod tests { }; let gas = calculate_key_authorization_gas(&scoped, &t1b_gas_params, TempoHardfork::T3); - // 1 key write + 14 scope slots: - // account mode(1) + target insert+mode(4) + selector insert+mode(4) + // 1 key write + 12 scope slots: + // account mode(1) + target vec push+mode(3) + selector vec push+mode(3) // + constrained selector recipient-length(1) + recipients values+positions(2*2). - let expected = ECRECOVER_GAS + sload + sstore * (1 + 14) + BUFFER; + let expected = ECRECOVER_GAS + sload + sstore * (1 + 12) + BUFFER; assert_eq!(gas, expected, "T3 scope writes should be fully charged"); } diff --git a/tips/tip-1011.md b/tips/tip-1011.md index 21caf69582..ae90c9385c 100644 --- a/tips/tip-1011.md +++ b/tips/tip-1011.md @@ -402,8 +402,8 @@ Definitions: Scoped-call storage writes counted for intrinsic gas: 1. Restricted-mode marker: `1` slot when `allowed_calls` is `Some(...)`. -2. Each target scope writes `4` slots: target-set length, target-set value, target-set position, and target mode. -3. Each selector rule writes `4` slots: selector-set length, selector-set value, selector-set position, and selector mode. +2. Each target scope writes `3` slots: target-vec length, target-vec value, and target mode. +3. Each selector rule writes `3` slots: selector-vec length, selector-vec value, and selector mode. 4. Each recipient-constrained selector writes `1` additional slot for recipient-set length. 5. Each recipient entry writes `2` slots: recipient-set value and recipient-set position. @@ -413,10 +413,10 @@ gas_key_authorization_new = gas_key_authorization_existing scope_slots = 0 if allowed_calls is None = 1 if allowed_calls is Some([]) // explicit restricted-mode marker - = 1 + 4*S + 4*K + C + 2*W if allowed_calls is Some(scopes) + = 1 + 3*S + 3*K + C + 2*W if allowed_calls is Some(scopes) ``` -Justification for `1 + 4*S + 4*K + C + 2*W`: `1` stores restricted mode, each target scope materializes as four set/mode writes, each selector rule materializes as four set/mode writes, each constrained selector writes one recipient-set length slot, and each recipient writes two set-membership slots. +Justification for `1 + 3*S + 3*K + C + 2*W`: `1` stores restricted mode, each target scope materializes as one vec length write, one vec element write, and one mode write, each selector rule materializes as one vec length write, one vec element write, and one mode write, each constrained selector writes one recipient-set length slot, and each recipient writes two set-membership slots. Bounds: @@ -525,14 +525,14 @@ Additive periodic-limit layout: | `spending_limits[account_key][token]` | `U256` | Remaining amount / `remainingInPeriod` | | `spending_limit_period_state[account_key][token]` | struct `{ max, period, period_end }` | Periodic limit metadata | -Call-scope storage is account-scoped and represented as nested sets/maps: +Call-scope storage is account-scoped and represented as nested vecs/maps/sets: | Path | Type | Description | |------|------|-------------| | `key_scopes[account_key].mode` | `u8` | `0 = unrestricted`, `1 = scoped`, `2 = deny-all` | -| `key_scopes[account_key].targets` | `Set
` | Scoped target membership | +| `key_scopes[account_key].targets` | `Vec
` | Ordered list of scoped targets | | `key_scopes[account_key].target_scopes[target].mode` | `u8` | `0 = none`, `1 = any-selector`, `2 = explicit-selector-rules` | -| `key_scopes[account_key].target_scopes[target].selectors` | `Set` | Explicit selector membership | +| `key_scopes[account_key].target_scopes[target].selectors` | `Vec` | Ordered list of explicit selectors | | `key_scopes[account_key].target_scopes[target].selector_scopes[selector].mode` | `u8` | `1 = any-recipient`, `2 = constrained-recipient-set` | | `key_scopes[account_key].target_scopes[target].selector_scopes[selector].recipients` | `Set
` | Selector-level recipient membership |