EIP-168, 169: Dust protection (#4757)
* Dust protection * Track touched accounts in the substate * Minor alterations
This commit is contained in:
@@ -315,6 +315,11 @@ impl Account {
|
||||
self.code_hash == SHA3_EMPTY
|
||||
}
|
||||
|
||||
/// Check if account is basic (Has no code).
|
||||
pub fn is_basic(&self) -> bool {
|
||||
self.code_hash == SHA3_EMPTY
|
||||
}
|
||||
|
||||
/// Return the storage root associated with this account or None if it has been altered via the overlay.
|
||||
pub fn storage_root(&self) -> Option<&H256> { if self.storage_is_clean() {Some(&self.storage_root)} else {None} }
|
||||
|
||||
|
||||
@@ -96,7 +96,11 @@ enum AccountState {
|
||||
/// Account entry can contain existing (`Some`) or non-existing
|
||||
/// account (`None`)
|
||||
struct AccountEntry {
|
||||
/// Account entry. `None` if account known to be non-existant.
|
||||
account: Option<Account>,
|
||||
/// Unmodified account balance.
|
||||
old_balance: Option<U256>,
|
||||
/// Entry state.
|
||||
state: AccountState,
|
||||
}
|
||||
|
||||
@@ -107,6 +111,10 @@ impl AccountEntry {
|
||||
self.state == AccountState::Dirty
|
||||
}
|
||||
|
||||
fn exists_and_is_null(&self) -> bool {
|
||||
self.account.as_ref().map_or(false, |a| a.is_null())
|
||||
}
|
||||
|
||||
/// Clone dirty data into new `AccountEntry`. This includes
|
||||
/// basic account data and modified storage keys.
|
||||
/// Returns None if clean.
|
||||
@@ -121,6 +129,7 @@ impl AccountEntry {
|
||||
/// basic account data and modified storage keys.
|
||||
fn clone_dirty(&self) -> AccountEntry {
|
||||
AccountEntry {
|
||||
old_balance: self.old_balance,
|
||||
account: self.account.as_ref().map(Account::clone_dirty),
|
||||
state: self.state,
|
||||
}
|
||||
@@ -129,6 +138,7 @@ impl AccountEntry {
|
||||
// Create a new account entry and mark it as dirty.
|
||||
fn new_dirty(account: Option<Account>) -> AccountEntry {
|
||||
AccountEntry {
|
||||
old_balance: account.as_ref().map(|a| a.balance().clone()),
|
||||
account: account,
|
||||
state: AccountState::Dirty,
|
||||
}
|
||||
@@ -137,6 +147,7 @@ impl AccountEntry {
|
||||
// Create a new account entry and mark it as clean.
|
||||
fn new_clean(account: Option<Account>) -> AccountEntry {
|
||||
AccountEntry {
|
||||
old_balance: account.as_ref().map(|a| a.balance().clone()),
|
||||
account: account,
|
||||
state: AccountState::CleanFresh,
|
||||
}
|
||||
@@ -145,6 +156,7 @@ impl AccountEntry {
|
||||
// Create a new account entry and mark it as clean and cached.
|
||||
fn new_clean_cached(account: Option<Account>) -> AccountEntry {
|
||||
AccountEntry {
|
||||
old_balance: account.as_ref().map(|a| a.balance().clone()),
|
||||
account: account,
|
||||
state: AccountState::CleanCached,
|
||||
}
|
||||
@@ -181,7 +193,7 @@ pub fn check_proof(
|
||||
let res = State::from_existing(
|
||||
backend,
|
||||
root,
|
||||
engine.account_start_nonce(),
|
||||
engine.account_start_nonce(env_info.number),
|
||||
factories
|
||||
);
|
||||
|
||||
@@ -266,8 +278,8 @@ pub enum CleanupMode<'a> {
|
||||
ForceCreate,
|
||||
/// Don't delete null accounts upon touching, but also don't create them.
|
||||
NoEmpty,
|
||||
/// Add encountered null accounts to the provided kill-set, to be deleted later.
|
||||
KillEmpty(&'a mut HashSet<Address>),
|
||||
/// Mark all touched accounts.
|
||||
TrackTouched(&'a mut HashSet<Address>),
|
||||
}
|
||||
|
||||
const SEC_TRIE_DB_UNWRAP_STR: &'static str = "A state can only be created with valid root. Creating a SecTrieDB with a valid root will not fail. \
|
||||
@@ -549,31 +561,30 @@ impl<B: Backend> State<B> {
|
||||
let is_value_transfer = !incr.is_zero();
|
||||
if is_value_transfer || (cleanup_mode == CleanupMode::ForceCreate && !self.exists(a)?) {
|
||||
self.require(a, false)?.add_balance(incr);
|
||||
} else {
|
||||
match cleanup_mode {
|
||||
CleanupMode::KillEmpty(set) => if !is_value_transfer && self.exists(a)? && !self.exists_and_not_null(a)? {
|
||||
set.insert(a.clone());
|
||||
},
|
||||
_ => {}
|
||||
} else if let CleanupMode::TrackTouched(set) = cleanup_mode {
|
||||
if self.exists(a)? {
|
||||
set.insert(*a);
|
||||
self.touch(a)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Subtract `decr` from the balance of account `a`.
|
||||
pub fn sub_balance(&mut self, a: &Address, decr: &U256) -> trie::Result<()> {
|
||||
pub fn sub_balance(&mut self, a: &Address, decr: &U256, cleanup_mode: &mut CleanupMode) -> trie::Result<()> {
|
||||
trace!(target: "state", "sub_balance({}, {}): {}", a, decr, self.balance(a)?);
|
||||
if !decr.is_zero() || !self.exists(a)? {
|
||||
self.require(a, false)?.sub_balance(decr);
|
||||
}
|
||||
|
||||
if let CleanupMode::TrackTouched(ref mut set) = *cleanup_mode {
|
||||
set.insert(*a);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Subtracts `by` from the balance of `from` and adds it to that of `to`.
|
||||
pub fn transfer_balance(&mut self, from: &Address, to: &Address, by: &U256, cleanup_mode: CleanupMode) -> trie::Result<()> {
|
||||
self.sub_balance(from, by)?;
|
||||
pub fn transfer_balance(&mut self, from: &Address, to: &Address, by: &U256, mut cleanup_mode: CleanupMode) -> trie::Result<()> {
|
||||
self.sub_balance(from, by, &mut cleanup_mode)?;
|
||||
self.add_balance(to, by, cleanup_mode)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -640,33 +651,33 @@ impl<B: Backend> State<B> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit accounts to SecTrieDBMut. This is similar to cpp-ethereum's dev::eth::commit.
|
||||
/// `accounts` is mutable because we may need to commit the code or storage and record that.
|
||||
fn touch(&mut self, a: &Address) -> trie::Result<()> {
|
||||
self.require(a, false)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Commits our cached account changes into the trie.
|
||||
#[cfg_attr(feature="dev", allow(match_ref_pats))]
|
||||
#[cfg_attr(feature="dev", allow(needless_borrow))]
|
||||
fn commit_into(
|
||||
factories: &Factories,
|
||||
db: &mut B,
|
||||
root: &mut H256,
|
||||
accounts: &mut HashMap<Address, AccountEntry>
|
||||
) -> Result<(), Error> {
|
||||
pub fn commit(&mut self) -> Result<(), Error> {
|
||||
// first, commit the sub trees.
|
||||
let mut accounts = self.cache.borrow_mut();
|
||||
for (address, ref mut a) in accounts.iter_mut().filter(|&(_, ref a)| a.is_dirty()) {
|
||||
if let Some(ref mut account) = a.account {
|
||||
let addr_hash = account.address_hash(address);
|
||||
{
|
||||
let mut account_db = factories.accountdb.create(db.as_hashdb_mut(), addr_hash);
|
||||
account.commit_storage(&factories.trie, account_db.as_hashdb_mut())?;
|
||||
let mut account_db = self.factories.accountdb.create(self.db.as_hashdb_mut(), addr_hash);
|
||||
account.commit_storage(&self.factories.trie, account_db.as_hashdb_mut())?;
|
||||
account.commit_code(account_db.as_hashdb_mut());
|
||||
}
|
||||
if !account.is_empty() {
|
||||
db.note_non_null_account(address);
|
||||
self.db.note_non_null_account(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut trie = factories.trie.from_existing(db.as_hashdb_mut(), root)?;
|
||||
let mut trie = self.factories.trie.from_existing(self.db.as_hashdb_mut(), &mut self.root)?;
|
||||
for (address, ref mut a) in accounts.iter_mut().filter(|&(_, ref a)| a.is_dirty()) {
|
||||
a.state = AccountState::Committed;
|
||||
match a.account {
|
||||
@@ -676,7 +687,7 @@ impl<B: Backend> State<B> {
|
||||
None => {
|
||||
trie.remove(address)?;
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,17 +703,30 @@ impl<B: Backend> State<B> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Commits our cached account changes into the trie.
|
||||
pub fn commit(&mut self) -> Result<(), Error> {
|
||||
assert!(self.checkpoints.borrow().is_empty());
|
||||
Self::commit_into(&self.factories, &mut self.db, &mut self.root, &mut *self.cache.borrow_mut())
|
||||
}
|
||||
|
||||
/// Clear state cache
|
||||
pub fn clear(&mut self) {
|
||||
self.cache.borrow_mut().clear();
|
||||
}
|
||||
|
||||
/// Remove any touched empty or dust accounts.
|
||||
pub fn kill_garbage(&mut self, touched: &HashSet<Address>, remove_empty_touched: bool, min_balance: &Option<U256>, kill_contracts: bool) -> trie::Result<()> {
|
||||
let to_kill: HashSet<_> = {
|
||||
self.cache.borrow().iter().filter_map(|(address, ref entry)|
|
||||
if touched.contains(address) && // Check all touched accounts
|
||||
((remove_empty_touched && entry.exists_and_is_null()) // Remove all empty touched accounts.
|
||||
|| min_balance.map_or(false, |ref balance| entry.account.as_ref().map_or(false, |account|
|
||||
(account.is_basic() || kill_contracts) // Remove all basic and optionally contract accounts where balance has been decreased.
|
||||
&& account.balance() < balance && entry.old_balance.as_ref().map_or(false, |b| account.balance() < b)))) {
|
||||
|
||||
Some(address.clone())
|
||||
} else { None }).collect()
|
||||
};
|
||||
for address in to_kill {
|
||||
self.kill_account(&address);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "json-tests")]
|
||||
/// Populate the state from `accounts`.
|
||||
@@ -1926,7 +1950,7 @@ mod tests {
|
||||
assert_eq!(state.balance(&a).unwrap(), U256::from(69u64));
|
||||
state.commit().unwrap();
|
||||
assert_eq!(state.balance(&a).unwrap(), U256::from(69u64));
|
||||
state.sub_balance(&a, &U256::from(42u64)).unwrap();
|
||||
state.sub_balance(&a, &U256::from(42u64), &mut CleanupMode::NoEmpty).unwrap();
|
||||
assert_eq!(state.balance(&a).unwrap(), U256::from(27u64));
|
||||
state.commit().unwrap();
|
||||
assert_eq!(state.balance(&a).unwrap(), U256::from(27u64));
|
||||
@@ -2026,4 +2050,44 @@ mod tests {
|
||||
new_state.diff_from(state).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_kill_garbage() {
|
||||
let a = 10.into();
|
||||
let b = 20.into();
|
||||
let c = 30.into();
|
||||
let d = 40.into();
|
||||
let e = 50.into();
|
||||
let x = 0.into();
|
||||
let db = get_temp_state_db();
|
||||
let (root, db) = {
|
||||
let mut state = State::new(db, U256::from(0), Default::default());
|
||||
state.add_balance(&a, &U256::default(), CleanupMode::ForceCreate).unwrap(); // create an empty account
|
||||
state.add_balance(&b, &100.into(), CleanupMode::ForceCreate).unwrap(); // create a dust account
|
||||
state.add_balance(&c, &101.into(), CleanupMode::ForceCreate).unwrap(); // create a normal account
|
||||
state.add_balance(&d, &99.into(), CleanupMode::ForceCreate).unwrap(); // create another dust account
|
||||
state.new_contract(&e, 100.into(), 1.into()); // create a contract account
|
||||
state.init_code(&e, vec![0x00]).unwrap();
|
||||
state.commit().unwrap();
|
||||
state.drop()
|
||||
};
|
||||
|
||||
let mut state = State::from_existing(db, root, U256::from(0u8), Default::default()).unwrap();
|
||||
let mut touched = HashSet::new();
|
||||
state.add_balance(&a, &U256::default(), CleanupMode::TrackTouched(&mut touched)).unwrap(); // touch an account
|
||||
state.transfer_balance(&b, &x, &1.into(), CleanupMode::TrackTouched(&mut touched)).unwrap(); // touch an account decreasing its balance
|
||||
state.transfer_balance(&c, &x, &1.into(), CleanupMode::TrackTouched(&mut touched)).unwrap(); // touch an account decreasing its balance
|
||||
state.transfer_balance(&e, &x, &1.into(), CleanupMode::TrackTouched(&mut touched)).unwrap(); // touch an account decreasing its balance
|
||||
state.kill_garbage(&touched, true, &None, false).unwrap();
|
||||
assert!(!state.exists(&a).unwrap());
|
||||
assert!(state.exists(&b).unwrap());
|
||||
state.kill_garbage(&touched, true, &Some(100.into()), false).unwrap();
|
||||
assert!(!state.exists(&b).unwrap());
|
||||
assert!(state.exists(&c).unwrap());
|
||||
assert!(state.exists(&d).unwrap());
|
||||
assert!(state.exists(&e).unwrap());
|
||||
state.kill_garbage(&touched, true, &Some(100.into()), true).unwrap();
|
||||
assert!(state.exists(&c).unwrap());
|
||||
assert!(state.exists(&d).unwrap());
|
||||
assert!(!state.exists(&e).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
use std::collections::HashSet;
|
||||
use util::{Address, U256};
|
||||
use log_entry::LogEntry;
|
||||
use evm::Schedule;
|
||||
use evm::{Schedule, CleanDustMode};
|
||||
use super::CleanupMode;
|
||||
|
||||
/// State changes which should be applied in finalize,
|
||||
@@ -28,8 +28,8 @@ pub struct Substate {
|
||||
/// Any accounts that have suicided.
|
||||
pub suicides: HashSet<Address>,
|
||||
|
||||
/// Any accounts that are tagged for garbage collection.
|
||||
pub garbage: HashSet<Address>,
|
||||
/// Any accounts that are touched.
|
||||
pub touched: HashSet<Address>,
|
||||
|
||||
/// Any logs.
|
||||
pub logs: Vec<LogEntry>,
|
||||
@@ -50,7 +50,7 @@ impl Substate {
|
||||
/// Merge secondary substate `s` into self, accruing each element correspondingly.
|
||||
pub fn accrue(&mut self, s: Substate) {
|
||||
self.suicides.extend(s.suicides.into_iter());
|
||||
self.garbage.extend(s.garbage.into_iter());
|
||||
self.touched.extend(s.touched.into_iter());
|
||||
self.logs.extend(s.logs.into_iter());
|
||||
self.sstore_clears_count = self.sstore_clears_count + s.sstore_clears_count;
|
||||
self.contracts_created.extend(s.contracts_created.into_iter());
|
||||
@@ -59,10 +59,10 @@ impl Substate {
|
||||
/// Get the cleanup mode object from this.
|
||||
#[cfg_attr(feature="dev", allow(wrong_self_convention))]
|
||||
pub fn to_cleanup_mode(&mut self, schedule: &Schedule) -> CleanupMode {
|
||||
match (schedule.no_empty, schedule.kill_empty) {
|
||||
(false, _) => CleanupMode::ForceCreate,
|
||||
(true, false) => CleanupMode::NoEmpty,
|
||||
(true, true) => CleanupMode::KillEmpty(&mut self.garbage),
|
||||
match (schedule.kill_dust != CleanDustMode::Off, schedule.no_empty, schedule.kill_empty) {
|
||||
(false, false, _) => CleanupMode::ForceCreate,
|
||||
(false, true, false) => CleanupMode::NoEmpty,
|
||||
(false, true, true) | (true, _, _,) => CleanupMode::TrackTouched(&mut self.touched),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user