EIP2929 with journaling + Yolov3 (#79)
This commit is contained in:
260
ethcore/vm/src/access_list.rs
Normal file
260
ethcore/vm/src/access_list.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
use ethereum_types::{Address, H256};
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::HashMap,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
// Implementation of a hasheable borrowed pair
|
||||
trait KeyPair<A, B> {
|
||||
fn a(&self) -> &A;
|
||||
fn b(&self) -> &B;
|
||||
}
|
||||
impl<'a, A, B> Borrow<dyn KeyPair<A, B> + 'a> for (A, B)
|
||||
where
|
||||
A: Eq + Hash + 'a,
|
||||
B: Eq + Hash + 'a,
|
||||
{
|
||||
fn borrow(&self) -> &(dyn KeyPair<A, B> + 'a) {
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<A: Hash, B: Hash> Hash for (dyn KeyPair<A, B> + '_) {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.a().hash(state);
|
||||
self.b().hash(state);
|
||||
}
|
||||
}
|
||||
impl<A: Eq, B: Eq> PartialEq for (dyn KeyPair<A, B> + '_) {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.a() == other.a() && self.b() == other.b()
|
||||
}
|
||||
}
|
||||
impl<A: Eq, B: Eq> Eq for (dyn KeyPair<A, B> + '_) {}
|
||||
impl<A, B> KeyPair<A, B> for (A, B) {
|
||||
fn a(&self) -> &A {
|
||||
&self.0
|
||||
}
|
||||
fn b(&self) -> &B {
|
||||
&self.1
|
||||
}
|
||||
}
|
||||
impl<A, B> KeyPair<A, B> for (&A, &B) {
|
||||
fn a(&self) -> &A {
|
||||
self.0
|
||||
}
|
||||
fn b(&self) -> &B {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Journal {
|
||||
enabled: bool,
|
||||
last_id: usize,
|
||||
addresses: HashMap<Address, usize>,
|
||||
storage_keys: HashMap<(Address, H256), usize>,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct AccessList {
|
||||
id: usize,
|
||||
journal: Rc<RefCell<Journal>>,
|
||||
}
|
||||
|
||||
impl Clone for AccessList {
|
||||
fn clone(&self) -> Self {
|
||||
let mut journal = self.journal.as_ref().borrow_mut();
|
||||
let id = journal.last_id + 1;
|
||||
journal.last_id = id;
|
||||
Self {
|
||||
id: id,
|
||||
journal: self.journal.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AccessList {
|
||||
fn default() -> Self {
|
||||
AccessList::new(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AccessList {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let journal = self.journal.as_ref().borrow();
|
||||
for (addr, id) in journal.addresses.iter() {
|
||||
write!(f, "| ADDR {} -> {}\n", addr, id)?;
|
||||
}
|
||||
for ((addr, slot), id) in journal.storage_keys.iter() {
|
||||
write!(f, "| SLOT {}:{} -> {}\n", addr, slot, id)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl AccessList {
|
||||
/// Returns if the list is enabled
|
||||
pub fn new(enabled: bool) -> Self {
|
||||
let journal = Journal {
|
||||
enabled,
|
||||
last_id: 0,
|
||||
addresses: HashMap::new(),
|
||||
storage_keys: HashMap::new(),
|
||||
};
|
||||
Self {
|
||||
id: 0,
|
||||
journal: Rc::new(RefCell::new(journal)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if the list is enabled
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
let journal = self.journal.as_ref().borrow();
|
||||
journal.enabled
|
||||
}
|
||||
|
||||
/// Enable the access list control
|
||||
pub fn enable(&mut self) {
|
||||
let mut journal = self.journal.as_ref().borrow_mut();
|
||||
journal.enabled = true;
|
||||
}
|
||||
|
||||
/// Checks if contains an storage key
|
||||
pub fn contains_storage_key(&self, address: &Address, key: &H256) -> bool {
|
||||
let journal = self.journal.as_ref().borrow();
|
||||
if journal.enabled {
|
||||
journal
|
||||
.storage_keys
|
||||
.contains_key(&(address, key) as &dyn KeyPair<Address, H256>)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a storage key
|
||||
pub fn insert_storage_key(&mut self, address: Address, key: H256) {
|
||||
let mut journal = self.journal.as_ref().borrow_mut();
|
||||
if journal.enabled
|
||||
&& !journal
|
||||
.storage_keys
|
||||
.contains_key(&(address, key) as &dyn KeyPair<Address, H256>)
|
||||
{
|
||||
journal.storage_keys.insert((address, key), self.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if contains an address
|
||||
pub fn contains_address(&self, address: &Address) -> bool {
|
||||
let journal = self.journal.as_ref().borrow();
|
||||
if journal.enabled {
|
||||
journal.addresses.contains_key(&address)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
/// Inserts an address
|
||||
pub fn insert_address(&mut self, address: Address) {
|
||||
let mut journal = self.journal.as_ref().borrow_mut();
|
||||
if journal.enabled && !journal.addresses.contains_key(&address) {
|
||||
journal.addresses.insert(address, self.id);
|
||||
}
|
||||
}
|
||||
/// Removes all changes in journal
|
||||
pub fn rollback(&self) {
|
||||
let mut journal = self.journal.as_ref().borrow_mut();
|
||||
// `id < self.id` instead `id != self.if` is to take care about recursive calls
|
||||
journal.addresses.retain(|_, id| *id < self.id);
|
||||
journal.storage_keys.retain(|_, id| *id < self.id);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn default_accesslist_is_disabled() {
|
||||
let access_list = AccessList::default();
|
||||
assert_eq!(false, access_list.is_enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_disabled_accesslist_does_nothing() {
|
||||
let mut access_list = AccessList::default();
|
||||
access_list.insert_address(Address::from(1));
|
||||
access_list.insert_storage_key(Address::from(2), H256::from(3));
|
||||
assert_eq!(false, access_list.contains_address(&Address::from(1)));
|
||||
assert_eq!(
|
||||
false,
|
||||
access_list.contains_storage_key(&Address::from(2), &H256::from(3))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_enabled_accesslist_registers() {
|
||||
let mut access_list = AccessList::default();
|
||||
access_list.enable();
|
||||
assert_eq!(true, access_list.is_enabled());
|
||||
access_list.insert_address(Address::from(1));
|
||||
access_list.insert_storage_key(Address::from(2), H256::from(3));
|
||||
assert_eq!(true, access_list.contains_address(&Address::from(1)));
|
||||
assert_eq!(
|
||||
true,
|
||||
access_list.contains_storage_key(&Address::from(2), &H256::from(3))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cloned_accesslist_registers_in_parent() {
|
||||
let mut access_list = AccessList::default();
|
||||
access_list.enable();
|
||||
assert_eq!(true, access_list.is_enabled());
|
||||
access_list.insert_address(Address::from(1));
|
||||
access_list.insert_storage_key(Address::from(2), H256::from(3));
|
||||
|
||||
let mut access_list_call = access_list.clone();
|
||||
assert_eq!(true, access_list_call.contains_address(&Address::from(1)));
|
||||
assert_eq!(
|
||||
true,
|
||||
access_list_call.contains_storage_key(&Address::from(2), &H256::from(3))
|
||||
);
|
||||
access_list.insert_address(Address::from(4));
|
||||
assert_eq!(true, access_list_call.contains_address(&Address::from(4)));
|
||||
|
||||
assert_eq!(true, access_list.contains_address(&Address::from(4)));
|
||||
}
|
||||
#[test]
|
||||
fn cloned_accesslist_rollbacks_in_parent() {
|
||||
let mut access_list = AccessList::default();
|
||||
access_list.enable();
|
||||
assert_eq!(true, access_list.is_enabled());
|
||||
access_list.insert_address(Address::from(1));
|
||||
access_list.insert_storage_key(Address::from(2), H256::from(3));
|
||||
|
||||
let mut access_list_call = access_list.clone();
|
||||
access_list_call.insert_address(Address::from(1));
|
||||
access_list_call.insert_storage_key(Address::from(2), H256::from(3));
|
||||
access_list_call.insert_address(Address::from(4));
|
||||
|
||||
let mut access_list_call_call = access_list.clone();
|
||||
access_list_call_call.insert_address(Address::from(1));
|
||||
access_list_call_call.insert_storage_key(Address::from(2), H256::from(3));
|
||||
access_list_call_call.insert_address(Address::from(5));
|
||||
access_list_call_call.insert_storage_key(Address::from(6), H256::from(7));
|
||||
|
||||
access_list_call.rollback();
|
||||
|
||||
assert_eq!(true, access_list.contains_address(&Address::from(1)));
|
||||
assert_eq!(false, access_list.contains_address(&Address::from(4)));
|
||||
assert_eq!(false, access_list.contains_address(&Address::from(5)));
|
||||
assert_eq!(
|
||||
true,
|
||||
access_list.contains_storage_key(&Address::from(2), &H256::from(3))
|
||||
);
|
||||
assert_eq!(
|
||||
false,
|
||||
access_list.contains_storage_key(&Address::from(6), &H256::from(7))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,13 @@
|
||||
// along with OpenEthereum. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Evm input params.
|
||||
use super::access_list::AccessList;
|
||||
use bytes::Bytes;
|
||||
use call_type::CallType;
|
||||
use ethereum_types::{Address, H256, U256};
|
||||
use ethjson;
|
||||
use hash::{keccak, KECCAK_EMPTY};
|
||||
|
||||
use call_type::CallType;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Transaction value
|
||||
@@ -90,6 +90,8 @@ pub struct ActionParams {
|
||||
pub call_type: CallType,
|
||||
/// Param types encoding
|
||||
pub params_type: ParamsType,
|
||||
/// Current access list
|
||||
pub access_list: AccessList,
|
||||
}
|
||||
|
||||
impl Default for ActionParams {
|
||||
@@ -108,6 +110,7 @@ impl Default for ActionParams {
|
||||
data: None,
|
||||
call_type: CallType::None,
|
||||
params_type: ParamsType::Separate,
|
||||
access_list: AccessList::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,6 +134,7 @@ impl From<ethjson::vm::Transaction> for ActionParams {
|
||||
false => CallType::Call,
|
||||
}, // TODO @debris is this correct?
|
||||
params_type: ParamsType::Separate,
|
||||
access_list: AccessList::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +101,9 @@ pub trait Ext {
|
||||
trap: bool,
|
||||
) -> ::std::result::Result<ContractCreateResult, TrapKind>;
|
||||
|
||||
/// Returns the address that will be created in the create call
|
||||
fn calc_address(&self, code: &[u8], address: CreateContractAddress) -> Option<Address>;
|
||||
|
||||
/// Message call.
|
||||
///
|
||||
/// Returns Err, if we run out of gas.
|
||||
@@ -184,4 +187,19 @@ pub trait Ext {
|
||||
|
||||
/// Check if running in static context.
|
||||
fn is_static(&self) -> bool;
|
||||
|
||||
/// Returns if the list is enabled
|
||||
fn al_is_enabled(&self) -> bool;
|
||||
|
||||
/// Checks if contains an storage key
|
||||
fn al_contains_storage_key(&self, address: &Address, key: &H256) -> bool;
|
||||
|
||||
/// Inserts an storage key into the list
|
||||
fn al_insert_storage_key(&mut self, address: Address, key: H256);
|
||||
|
||||
/// Checks if contains an address
|
||||
fn al_contains_address(&self, address: &Address) -> bool;
|
||||
|
||||
/// Inserts an address into the list
|
||||
fn al_insert_address(&mut self, address: Address);
|
||||
}
|
||||
|
||||
@@ -23,16 +23,18 @@ extern crate parity_bytes as bytes;
|
||||
extern crate patricia_trie_ethereum as ethtrie;
|
||||
extern crate rlp;
|
||||
|
||||
pub mod access_list;
|
||||
mod action_params;
|
||||
mod call_type;
|
||||
mod env_info;
|
||||
mod error;
|
||||
mod ext;
|
||||
mod return_data;
|
||||
mod schedule;
|
||||
pub mod schedule;
|
||||
|
||||
pub mod tests;
|
||||
|
||||
pub use access_list::AccessList;
|
||||
pub use action_params::{ActionParams, ActionValue, ParamsType};
|
||||
pub use call_type::CallType;
|
||||
pub use env_info::{EnvInfo, LastHashes};
|
||||
@@ -42,7 +44,7 @@ pub use return_data::{GasLeft, ReturnData};
|
||||
pub use schedule::{CleanDustMode, Schedule, WasmCosts};
|
||||
|
||||
/// Virtual Machine interface
|
||||
pub trait Exec: Send {
|
||||
pub trait Exec {
|
||||
/// This function should be used to execute transaction.
|
||||
/// It returns either an error, a known amount of gas left, or parameters to be used
|
||||
/// to compute the final gas left.
|
||||
@@ -50,13 +52,13 @@ pub trait Exec: Send {
|
||||
}
|
||||
|
||||
/// Resume call interface
|
||||
pub trait ResumeCall: Send {
|
||||
pub trait ResumeCall {
|
||||
/// Resume an execution for call, returns back the Vm interface.
|
||||
fn resume_call(self: Box<Self>, result: MessageCallResult) -> Box<dyn Exec>;
|
||||
}
|
||||
|
||||
/// Resume create interface
|
||||
pub trait ResumeCreate: Send {
|
||||
pub trait ResumeCreate {
|
||||
/// Resume an execution from create, returns back the Vm interface.
|
||||
fn resume_create(self: Box<Self>, result: ContractCreateResult) -> Box<dyn Exec>;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,15 @@
|
||||
|
||||
//! Cost schedule and other parameterisations for the EVM.
|
||||
|
||||
// Gas per non accessed address when sload
|
||||
pub const EIP2929_COLD_SLOAD_COST: usize = 2100;
|
||||
// Gas per non accessed address accessing account from other opcodes defined in EIP2929
|
||||
pub const EIP2929_COLD_ACCOUNT_ACCESS_COST: usize = 2600;
|
||||
// Gas per already accessed address
|
||||
pub const EIP2929_WARM_STORAGE_READ_COST: usize = 100;
|
||||
// Gas per sstore reset
|
||||
pub const EIP2929_SSTORE_RESET_GAS: usize = 5000 - EIP2929_COLD_SLOAD_COST;
|
||||
|
||||
/// Definition of the cost schedule and other parameterisations for the EVM.
|
||||
#[derive(Debug)]
|
||||
pub struct Schedule {
|
||||
@@ -63,6 +72,12 @@ pub struct Schedule {
|
||||
pub create_gas: usize,
|
||||
/// Gas price for `*CALL*` opcodes
|
||||
pub call_gas: usize,
|
||||
/// EIP-2929 COLD_SLOAD_COST
|
||||
pub cold_sload_cost: usize,
|
||||
/// EIP-2929 COLD_ACCOUNT_ACCESS_COST
|
||||
pub cold_account_access_cost: usize,
|
||||
/// EIP-2929 WARM_STORAGE_READ_COST
|
||||
pub warm_storage_read_cost: usize,
|
||||
/// Stipend for transfer for `CALL|CALLCODE` opcode when `value>0`
|
||||
pub call_stipend: usize,
|
||||
/// Additional gas required for value transfer (`CALL|CALLCODE`)
|
||||
@@ -132,6 +147,8 @@ pub struct Schedule {
|
||||
pub keep_unsigned_nonce: bool,
|
||||
/// Wasm extra schedule settings, if wasm activated
|
||||
pub wasm: Option<WasmCosts>,
|
||||
/// Enable EIP-2929 rules
|
||||
pub eip2929: bool,
|
||||
}
|
||||
|
||||
/// Wasm cost table
|
||||
@@ -245,6 +262,9 @@ impl Schedule {
|
||||
log_topic_gas: 375,
|
||||
create_gas: 32000,
|
||||
call_gas: 700,
|
||||
cold_account_access_cost: 0,
|
||||
cold_sload_cost: 0,
|
||||
warm_storage_read_cost: 0,
|
||||
call_stipend: 2300,
|
||||
call_value_transfer_gas: 9000,
|
||||
call_new_account_gas: 25000,
|
||||
@@ -274,6 +294,7 @@ impl Schedule {
|
||||
eip1706: false,
|
||||
keep_unsigned_nonce: false,
|
||||
wasm: None,
|
||||
eip2929: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +311,8 @@ impl Schedule {
|
||||
/// Schedule for the Constantinople fork of the Ethereum main net.
|
||||
pub fn new_constantinople() -> Schedule {
|
||||
let mut schedule = Self::new_byzantium();
|
||||
schedule.have_bitwise_shifting = true;
|
||||
schedule.have_bitwise_shifting = true; // EIP 145
|
||||
schedule.have_extcodehash = true; // EIP 1052
|
||||
schedule
|
||||
}
|
||||
|
||||
@@ -310,6 +332,30 @@ impl Schedule {
|
||||
pub fn new_berlin() -> Schedule {
|
||||
let mut schedule = Self::new_istanbul();
|
||||
schedule.have_subs = true; // EIP 2315
|
||||
|
||||
schedule
|
||||
}
|
||||
|
||||
/// Schedule for the Yolov3 testnet of the Ethereum main net.
|
||||
pub fn new_yolo3() -> Schedule {
|
||||
let mut schedule = Self::new_istanbul();
|
||||
schedule.have_subs = true; // EIP 2315
|
||||
|
||||
schedule.eip1283 = true;
|
||||
schedule.eip2929 = true;
|
||||
|
||||
schedule.cold_sload_cost = EIP2929_COLD_SLOAD_COST;
|
||||
schedule.cold_account_access_cost = EIP2929_COLD_ACCOUNT_ACCESS_COST;
|
||||
schedule.warm_storage_read_cost = EIP2929_WARM_STORAGE_READ_COST;
|
||||
|
||||
schedule.sload_gas = EIP2929_WARM_STORAGE_READ_COST;
|
||||
schedule.call_gas = EIP2929_COLD_ACCOUNT_ACCESS_COST;
|
||||
schedule.balance_gas = EIP2929_COLD_ACCOUNT_ACCESS_COST;
|
||||
schedule.extcodecopy_base_gas = EIP2929_COLD_ACCOUNT_ACCESS_COST;
|
||||
schedule.extcodehash_gas = EIP2929_COLD_ACCOUNT_ACCESS_COST;
|
||||
schedule.extcodesize_gas = EIP2929_COLD_ACCOUNT_ACCESS_COST;
|
||||
schedule.sstore_reset_gas = EIP2929_SSTORE_RESET_GAS;
|
||||
|
||||
schedule
|
||||
}
|
||||
|
||||
@@ -342,6 +388,9 @@ impl Schedule {
|
||||
log_topic_gas: 375,
|
||||
create_gas: 32000,
|
||||
call_gas: 40,
|
||||
cold_account_access_cost: 0,
|
||||
cold_sload_cost: 0,
|
||||
warm_storage_read_cost: 0,
|
||||
call_stipend: 2300,
|
||||
call_value_transfer_gas: 9000,
|
||||
call_new_account_gas: 25000,
|
||||
@@ -371,6 +420,7 @@ impl Schedule {
|
||||
eip1706: false,
|
||||
keep_unsigned_nonce: false,
|
||||
wasm: None,
|
||||
eip2929: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::access_list::AccessList;
|
||||
use bytes::Bytes;
|
||||
use error::TrapKind;
|
||||
use ethereum_types::{Address, H256, U256};
|
||||
@@ -75,6 +76,7 @@ pub struct FakeExt {
|
||||
pub balances: HashMap<Address, U256>,
|
||||
pub tracing: bool,
|
||||
pub is_static: bool,
|
||||
pub access_list: AccessList,
|
||||
|
||||
chain_id: u64,
|
||||
}
|
||||
@@ -122,6 +124,19 @@ impl FakeExt {
|
||||
ext
|
||||
}
|
||||
|
||||
/// New fake externalities with YoloV2 schedule rules
|
||||
pub fn new_yolo3(from: Address, to: Address, builtins: &[Address]) -> Self {
|
||||
let mut ext = FakeExt::default();
|
||||
ext.schedule = Schedule::new_yolo3();
|
||||
ext.access_list.enable();
|
||||
ext.access_list.insert_address(from);
|
||||
ext.access_list.insert_address(to);
|
||||
for builtin in builtins {
|
||||
ext.access_list.insert_address(*builtin);
|
||||
}
|
||||
ext
|
||||
}
|
||||
|
||||
/// Alter fake externalities to allow wasm
|
||||
pub fn with_wasm(mut self) -> Self {
|
||||
self.schedule.wasm = Some(Default::default());
|
||||
@@ -162,7 +177,7 @@ impl Ext for FakeExt {
|
||||
}
|
||||
|
||||
fn balance(&self, address: &Address) -> Result<U256> {
|
||||
Ok(self.balances[address])
|
||||
Ok(self.balances.get(address).cloned().unwrap_or(U256::zero()))
|
||||
}
|
||||
|
||||
fn blockhash(&mut self, number: &U256) -> H256 {
|
||||
@@ -191,6 +206,10 @@ impl Ext for FakeExt {
|
||||
Ok(ContractCreateResult::Failed)
|
||||
}
|
||||
|
||||
fn calc_address(&self, _code: &[u8], _address: CreateContractAddress) -> Option<Address> {
|
||||
None
|
||||
}
|
||||
|
||||
fn call(
|
||||
&mut self,
|
||||
gas: &U256,
|
||||
@@ -276,4 +295,24 @@ impl Ext for FakeExt {
|
||||
fn trace_next_instruction(&mut self, _pc: usize, _instruction: u8, _gas: U256) -> bool {
|
||||
self.tracing
|
||||
}
|
||||
|
||||
fn al_is_enabled(&self) -> bool {
|
||||
self.access_list.is_enabled()
|
||||
}
|
||||
|
||||
fn al_contains_storage_key(&self, address: &Address, key: &H256) -> bool {
|
||||
self.access_list.contains_storage_key(address, key)
|
||||
}
|
||||
|
||||
fn al_insert_storage_key(&mut self, address: Address, key: H256) {
|
||||
self.access_list.insert_storage_key(address, key)
|
||||
}
|
||||
|
||||
fn al_contains_address(&self, address: &Address) -> bool {
|
||||
self.access_list.contains_address(address)
|
||||
}
|
||||
|
||||
fn al_insert_address(&mut self, address: Address) {
|
||||
self.access_list.insert_address(address)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user