From ad63780b4dafc6230262c10dd416cde9e2a356d6 Mon Sep 17 00:00:00 2001 From: Arkadiy Paronyan Date: Tue, 27 Sep 2016 18:02:11 +0200 Subject: [PATCH] Canonical state cache (master) (#2311) * State cache * Reduced copying data between caches Whitespace and optional symbols * Reduced copying data between caches Whitespace and optional symbols * Set a limit on storage cache * Style and docs --- Cargo.lock | 14 + ethcore/Cargo.toml | 1 + ethcore/src/block.rs | 49 ++-- ethcore/src/client/client.rs | 40 +-- ethcore/src/client/test_client.rs | 10 +- ethcore/src/engines/basic_authority.rs | 4 +- ethcore/src/engines/instant_seal.rs | 4 +- ethcore/src/ethereum/ethash.rs | 8 +- ethcore/src/ethereum/mod.rs | 4 +- ethcore/src/evm/tests.rs | 2 +- ethcore/src/externalities.rs | 1 - ethcore/src/lib.rs | 2 + ethcore/src/pod_account.rs | 2 +- ethcore/src/snapshot/account.rs | 11 +- ethcore/src/spec/spec.rs | 11 +- ethcore/src/state/account.rs | 214 +++++++++++---- ethcore/src/state/mod.rs | 351 +++++++++++++++++++------ ethcore/src/state_db.rs | 161 ++++++++++++ ethcore/src/tests/helpers.rs | 18 +- logger/src/lib.rs | 4 +- 20 files changed, 701 insertions(+), 210 deletions(-) create mode 100644 ethcore/src/state_db.rs diff --git a/Cargo.lock b/Cargo.lock index 2ae27d202..8816cc517 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,6 +288,7 @@ dependencies = [ "hyper 0.9.4 (git+https://github.com/ethcore/hyper)", "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "lru-cache 0.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "rayon 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -863,6 +864,11 @@ name = "libc" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "linked-hash-map" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "linked-hash-map" version = "0.3.0" @@ -873,6 +879,14 @@ name = "log" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "lru-cache" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "linked-hash-map 0.0.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "matches" version = "0.1.2" diff --git a/ethcore/Cargo.toml b/ethcore/Cargo.toml index 3ad9e69c4..2f1291d56 100644 --- a/ethcore/Cargo.toml +++ b/ethcore/Cargo.toml @@ -37,6 +37,7 @@ ethkey = { path = "../ethkey" } ethcore-ipc-nano = { path = "../ipc/nano" } rlp = { path = "../util/rlp" } rand = "0.3" +lru-cache = "0.0.7" [dependencies.hyper] git = "https://github.com/ethcore/hyper" diff --git a/ethcore/src/block.rs b/ethcore/src/block.rs index b35b4dc1a..b297f178b 100644 --- a/ethcore/src/block.rs +++ b/ethcore/src/block.rs @@ -19,6 +19,7 @@ use common::*; use engines::Engine; use state::*; +use state_db::StateDB; use verification::PreverifiedBlock; use trace::FlatTrace; use factory::Factories; @@ -179,7 +180,7 @@ pub trait IsBlock { /// Trait for a object that has a state database. pub trait Drain { /// Drop this object and return the underlieing database. - fn drain(self) -> Box; + fn drain(self) -> StateDB; } impl IsBlock for ExecutedBlock { @@ -231,7 +232,7 @@ impl<'x> OpenBlock<'x> { engine: &'x Engine, factories: Factories, tracing: bool, - db: Box, + db: StateDB, parent: &Header, last_hashes: Arc, author: Address, @@ -474,7 +475,9 @@ impl LockedBlock { impl Drain for LockedBlock { /// Drop this object and return the underlieing database. - fn drain(self) -> Box { self.block.state.drop().1 } + fn drain(self) -> StateDB { + self.block.state.drop().1 + } } impl SealedBlock { @@ -490,7 +493,9 @@ impl SealedBlock { impl Drain for SealedBlock { /// Drop this object and return the underlieing database. - fn drain(self) -> Box { self.block.state.drop().1 } + fn drain(self) -> StateDB { + self.block.state.drop().1 + } } impl IsBlock for SealedBlock { @@ -505,7 +510,7 @@ pub fn enact( uncles: &[Header], engine: &Engine, tracing: bool, - db: Box, + db: StateDB, parent: &Header, last_hashes: Arc, factories: Factories, @@ -537,7 +542,7 @@ pub fn enact_bytes( block_bytes: &[u8], engine: &Engine, tracing: bool, - db: Box, + db: StateDB, parent: &Header, last_hashes: Arc, factories: Factories, @@ -553,7 +558,7 @@ pub fn enact_verified( block: &PreverifiedBlock, engine: &Engine, tracing: bool, - db: Box, + db: StateDB, parent: &Header, last_hashes: Arc, factories: Factories, @@ -568,7 +573,7 @@ pub fn enact_and_seal( block_bytes: &[u8], engine: &Engine, tracing: bool, - db: Box, + db: StateDB, parent: &Header, last_hashes: Arc, factories: Factories, @@ -588,9 +593,9 @@ mod tests { use spec::*; let spec = Spec::new_test(); let genesis_header = spec.genesis_header(); - let mut db_result = get_temp_journal_db(); + let mut db_result = get_temp_state_db(); let mut db = db_result.take(); - spec.ensure_db_good(db.as_hashdb_mut()).unwrap(); + spec.ensure_db_good(&mut db).unwrap(); let last_hashes = Arc::new(vec![genesis_header.hash()]); let b = OpenBlock::new(&*spec.engine, Default::default(), false, db, &genesis_header, last_hashes, Address::zero(), (3141562.into(), 31415620.into()), vec![]).unwrap(); let b = b.close_and_lock(); @@ -604,25 +609,25 @@ mod tests { let engine = &*spec.engine; let genesis_header = spec.genesis_header(); - let mut db_result = get_temp_journal_db(); + let mut db_result = get_temp_state_db(); let mut db = db_result.take(); - spec.ensure_db_good(db.as_hashdb_mut()).unwrap(); + spec.ensure_db_good(&mut db).unwrap(); let last_hashes = Arc::new(vec![genesis_header.hash()]); let b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes.clone(), Address::zero(), (3141562.into(), 31415620.into()), vec![]).unwrap() .close_and_lock().seal(engine, vec![]).unwrap(); let orig_bytes = b.rlp_bytes(); let orig_db = b.drain(); - let mut db_result = get_temp_journal_db(); + let mut db_result = get_temp_state_db(); let mut db = db_result.take(); - spec.ensure_db_good(db.as_hashdb_mut()).unwrap(); + spec.ensure_db_good(&mut db).unwrap(); let e = enact_and_seal(&orig_bytes, engine, false, db, &genesis_header, last_hashes, Default::default()).unwrap(); assert_eq!(e.rlp_bytes(), orig_bytes); let db = e.drain(); - assert_eq!(orig_db.keys(), db.keys()); - assert!(orig_db.keys().iter().filter(|k| orig_db.get(k.0) != db.get(k.0)).next() == None); + assert_eq!(orig_db.journal_db().keys(), db.journal_db().keys()); + assert!(orig_db.journal_db().keys().iter().filter(|k| orig_db.journal_db().get(k.0) != db.journal_db().get(k.0)).next() == None); } #[test] @@ -632,9 +637,9 @@ mod tests { let engine = &*spec.engine; let genesis_header = spec.genesis_header(); - let mut db_result = get_temp_journal_db(); + let mut db_result = get_temp_state_db(); let mut db = db_result.take(); - spec.ensure_db_good(db.as_hashdb_mut()).unwrap(); + spec.ensure_db_good(&mut db).unwrap(); let last_hashes = Arc::new(vec![genesis_header.hash()]); let mut open_block = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes.clone(), Address::zero(), (3141562.into(), 31415620.into()), vec![]).unwrap(); let mut uncle1_header = Header::new(); @@ -648,9 +653,9 @@ mod tests { let orig_bytes = b.rlp_bytes(); let orig_db = b.drain(); - let mut db_result = get_temp_journal_db(); + let mut db_result = get_temp_state_db(); let mut db = db_result.take(); - spec.ensure_db_good(db.as_hashdb_mut()).unwrap(); + spec.ensure_db_good(&mut db).unwrap(); let e = enact_and_seal(&orig_bytes, engine, false, db, &genesis_header, last_hashes, Default::default()).unwrap(); let bytes = e.rlp_bytes(); @@ -659,7 +664,7 @@ mod tests { assert_eq!(uncles[1].extra_data(), b"uncle2"); let db = e.drain(); - assert_eq!(orig_db.keys(), db.keys()); - assert!(orig_db.keys().iter().filter(|k| orig_db.get(k.0) != db.get(k.0)).next() == None); + assert_eq!(orig_db.journal_db().keys(), db.journal_db().keys()); + assert!(orig_db.journal_db().keys().iter().filter(|k| orig_db.journal_db().get(k.0) != db.journal_db().get(k.0)).next() == None); } } diff --git a/ethcore/src/client/client.rs b/ethcore/src/client/client.rs index befdc32c3..3dbf56e35 100644 --- a/ethcore/src/client/client.rs +++ b/ethcore/src/client/client.rs @@ -23,7 +23,7 @@ use time::precise_time_ns; // util use util::{Bytes, PerfTimer, Itertools, Mutex, RwLock}; -use util::journaldb::{self, JournalDB}; +use util::journaldb; use util::{U256, H256, Address, H2048, Uint}; use util::TrieFactory; use util::kvdb::*; @@ -65,7 +65,7 @@ use miner::{Miner, MinerService}; use snapshot::{self, io as snapshot_io}; use factory::Factories; use rlp::{View, UntrustedRlp}; - +use state_db::StateDB; // re-export pub use types::blockchain_info::BlockChainInfo; @@ -125,9 +125,9 @@ pub struct Client { tracedb: RwLock>, engine: Arc, config: ClientConfig, - db: RwLock>, pruning: journaldb::Algorithm, - state_db: RwLock>, + db: RwLock>, + state_db: Mutex, block_queue: BlockQueue, report: RwLock, import_lock: Mutex<()>, @@ -171,14 +171,15 @@ impl Client { let chain = Arc::new(BlockChain::new(config.blockchain.clone(), &gb, db.clone())); let tracedb = RwLock::new(TraceDB::new(config.tracing.clone(), db.clone(), chain.clone())); - let mut state_db = journaldb::new(db.clone(), config.pruning, ::db::COL_STATE); - if state_db.is_empty() && try!(spec.ensure_db_good(state_db.as_hashdb_mut())) { + let journal_db = journaldb::new(db.clone(), config.pruning, ::db::COL_STATE); + let mut state_db = StateDB::new(journal_db); + if state_db.journal_db().is_empty() && try!(spec.ensure_db_good(&mut state_db)) { let mut batch = DBTransaction::new(&db); try!(state_db.commit(&mut batch, 0, &spec.genesis_header().hash(), None)); try!(db.write(batch).map_err(ClientError::Database)); } - if !chain.block_header(&chain.best_block_hash()).map_or(true, |h| state_db.contains(h.state_root())) { + if !chain.block_header(&chain.best_block_hash()).map_or(true, |h| state_db.journal_db().contains(h.state_root())) { warn!("State root not found for block #{} ({})", chain.best_block_number(), chain.best_block_hash().hex()); } @@ -207,7 +208,7 @@ impl Client { verifier: verification::new(config.verifier_type.clone()), config: config, db: RwLock::new(db), - state_db: RwLock::new(state_db), + state_db: Mutex::new(state_db), block_queue: block_queue, report: RwLock::new(Default::default()), import_lock: Mutex::new(()), @@ -298,7 +299,8 @@ impl Client { // Enact Verified Block let parent = chain_has_parent.unwrap(); let last_hashes = self.build_last_hashes(header.parent_hash().clone()); - let db = self.state_db.read().boxed_clone(); + let is_canon = header.parent_hash() == &chain.best_block_hash(); + let db = if is_canon { self.state_db.lock().boxed_clone_canon() } else { self.state_db.lock().boxed_clone() }; let enact_result = enact_verified(block, engine, self.tracedb.read().tracing_enabled(), db, &parent, last_hashes, self.factories.clone()); if let Err(e) = enact_result { @@ -441,7 +443,8 @@ impl Client { // CHECK! I *think* this is fine, even if the state_root is equal to another // already-imported block of the same number. // TODO: Prove it with a test. - block.drain().commit(&mut batch, number, hash, ancient).expect("DB commit failed."); + let mut state = block.drain(); + state.commit(&mut batch, number, hash, ancient).expect("DB commit failed."); let route = chain.insert_block(&mut batch, block_data, receipts); self.tracedb.read().import(&mut batch, TraceImportRequest { @@ -454,7 +457,6 @@ impl Client { // Final commit to the DB self.db.read().write_buffered(batch); chain.commit(); - self.update_last_hashes(&parent, hash); route } @@ -496,7 +498,7 @@ impl Client { }; self.block_header(id).and_then(|header| { - let db = self.state_db.read().boxed_clone(); + let db = self.state_db.lock().boxed_clone(); // early exit for pruned blocks if db.is_pruned() && self.chain.read().best_block_number() >= block_number + HISTORY { @@ -527,7 +529,7 @@ impl Client { /// Get a copy of the best block's state. pub fn state(&self) -> State { State::from_existing( - self.state_db.read().boxed_clone(), + self.state_db.lock().boxed_clone(), HeaderView::new(&self.best_block_header()).state_root(), self.engine.account_start_nonce(), self.factories.clone()) @@ -542,7 +544,7 @@ impl Client { /// Get the report. pub fn report(&self) -> ClientReport { let mut report = self.report.read().clone(); - report.state_db_mem = self.state_db.read().mem_used(); + report.state_db_mem = self.state_db.lock().mem_used(); report } @@ -598,7 +600,7 @@ impl Client { /// Take a snapshot at the given block. /// If the ID given is "latest", this will default to 1000 blocks behind. pub fn take_snapshot(&self, writer: W, at: BlockID, p: &snapshot::Progress) -> Result<(), EthcoreError> { - let db = self.state_db.read().boxed_clone(); + let db = self.state_db.lock().journal_db().boxed_clone(); let best_block_number = self.chain_info().best_block_number; let block_number = try!(self.block_number(at).ok_or(snapshot::Error::InvalidStartingBlock(at))); @@ -677,14 +679,14 @@ impl snapshot::DatabaseRestore for Client { trace!(target: "snapshot", "Replacing client database with {:?}", new_db); let _import_lock = self.import_lock.lock(); - let mut state_db = self.state_db.write(); + let mut state_db = self.state_db.lock(); let mut chain = self.chain.write(); let mut tracedb = self.tracedb.write(); self.miner.clear(); let db = self.db.write(); try!(db.restore(new_db)); - *state_db = journaldb::new(db.clone(), self.pruning, ::db::COL_STATE); + *state_db = StateDB::new(journaldb::new(db.clone(), self.pruning, ::db::COL_STATE)); *chain = Arc::new(BlockChain::new(self.config.blockchain.clone(), &[], db.clone())); *tracedb = TraceDB::new(self.config.tracing.clone(), db.clone(), chain.clone()); Ok(()) @@ -908,7 +910,7 @@ impl BlockChainClient for Client { } fn state_data(&self, hash: &H256) -> Option { - self.state_db.read().state(hash) + self.state_db.lock().journal_db().state(hash) } fn block_receipts(&self, hash: &H256) -> Option { @@ -1050,7 +1052,7 @@ impl MiningBlockChainClient for Client { engine, self.factories.clone(), false, // TODO: this will need to be parameterised once we want to do immediate mining insertion. - self.state_db.read().boxed_clone(), + self.state_db.lock().boxed_clone(), &chain.block_header(&h).expect("h is best block hash: so its header must exist: qed"), self.build_last_hashes(h.clone()), author, diff --git a/ethcore/src/client/test_client.rs b/ethcore/src/client/test_client.rs index 480d49dc9..971f7c448 100644 --- a/ethcore/src/client/test_client.rs +++ b/ethcore/src/client/test_client.rs @@ -42,6 +42,7 @@ use block::{OpenBlock, SealedBlock}; use executive::Executed; use error::CallError; use trace::LocalizedTrace; +use state_db::StateDB; /// Test client. pub struct TestBlockChainClient { @@ -283,13 +284,14 @@ impl TestBlockChainClient { } } -pub fn get_temp_journal_db() -> GuardedTempResult> { +pub fn get_temp_state_db() -> GuardedTempResult { let temp = RandomTempPath::new(); let db = Database::open_default(temp.as_str()).unwrap(); let journal_db = journaldb::new(Arc::new(db), journaldb::Algorithm::EarlyMerge, None); + let state_db = StateDB::new(journal_db); GuardedTempResult { _temp: temp, - result: Some(journal_db) + result: Some(state_db) } } @@ -297,9 +299,9 @@ impl MiningBlockChainClient for TestBlockChainClient { fn prepare_open_block(&self, author: Address, gas_range_target: (U256, U256), extra_data: Bytes) -> OpenBlock { let engine = &*self.spec.engine; let genesis_header = self.spec.genesis_header(); - let mut db_result = get_temp_journal_db(); + let mut db_result = get_temp_state_db(); let mut db = db_result.take(); - self.spec.ensure_db_good(db.as_hashdb_mut()).unwrap(); + self.spec.ensure_db_good(&mut db).unwrap(); let last_hashes = vec![genesis_header.hash()]; let mut open_block = OpenBlock::new( diff --git a/ethcore/src/engines/basic_authority.rs b/ethcore/src/engines/basic_authority.rs index 8d852e99e..6e3c2f1dd 100644 --- a/ethcore/src/engines/basic_authority.rs +++ b/ethcore/src/engines/basic_authority.rs @@ -252,9 +252,9 @@ mod tests { let spec = new_test_authority(); let engine = &*spec.engine; let genesis_header = spec.genesis_header(); - let mut db_result = get_temp_journal_db(); + let mut db_result = get_temp_state_db(); let mut db = db_result.take(); - spec.ensure_db_good(db.as_hashdb_mut()).unwrap(); + spec.ensure_db_good(&mut db).unwrap(); let last_hashes = Arc::new(vec![genesis_header.hash()]); let b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes, addr, (3141562.into(), 31415620.into()), vec![]).unwrap(); let b = b.close_and_lock(); diff --git a/ethcore/src/engines/instant_seal.rs b/ethcore/src/engines/instant_seal.rs index 26d2ed5bf..174a80ea8 100644 --- a/ethcore/src/engines/instant_seal.rs +++ b/ethcore/src/engines/instant_seal.rs @@ -81,9 +81,9 @@ mod tests { let spec = Spec::new_test_instant(); let engine = &*spec.engine; let genesis_header = spec.genesis_header(); - let mut db_result = get_temp_journal_db(); + let mut db_result = get_temp_state_db(); let mut db = db_result.take(); - spec.ensure_db_good(db.as_hashdb_mut()).unwrap(); + spec.ensure_db_good(&mut db).unwrap(); let last_hashes = Arc::new(vec![genesis_header.hash()]); let b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes, addr, (3141562.into(), 31415620.into()), vec![]).unwrap(); let b = b.close_and_lock(); diff --git a/ethcore/src/ethereum/ethash.rs b/ethcore/src/ethereum/ethash.rs index 734acb758..249a3e851 100644 --- a/ethcore/src/ethereum/ethash.rs +++ b/ethcore/src/ethereum/ethash.rs @@ -355,9 +355,9 @@ mod tests { let spec = new_morden(); let engine = &*spec.engine; let genesis_header = spec.genesis_header(); - let mut db_result = get_temp_journal_db(); + let mut db_result = get_temp_state_db(); let mut db = db_result.take(); - spec.ensure_db_good(db.as_hashdb_mut()).unwrap(); + spec.ensure_db_good(&mut db).unwrap(); let last_hashes = Arc::new(vec![genesis_header.hash()]); let b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes, Address::zero(), (3141562.into(), 31415620.into()), vec![]).unwrap(); let b = b.close(); @@ -369,9 +369,9 @@ mod tests { let spec = new_morden(); let engine = &*spec.engine; let genesis_header = spec.genesis_header(); - let mut db_result = get_temp_journal_db(); + let mut db_result = get_temp_state_db(); let mut db = db_result.take(); - spec.ensure_db_good(db.as_hashdb_mut()).unwrap(); + spec.ensure_db_good(&mut db).unwrap(); let last_hashes = Arc::new(vec![genesis_header.hash()]); let mut b = OpenBlock::new(engine, Default::default(), false, db, &genesis_header, last_hashes, Address::zero(), (3141562.into(), 31415620.into()), vec![]).unwrap(); let mut uncle = Header::new(); diff --git a/ethcore/src/ethereum/mod.rs b/ethcore/src/ethereum/mod.rs index 6d46d5551..01ef81c75 100644 --- a/ethcore/src/ethereum/mod.rs +++ b/ethcore/src/ethereum/mod.rs @@ -69,9 +69,9 @@ mod tests { let spec = new_morden(); let engine = &spec.engine; let genesis_header = spec.genesis_header(); - let mut db_result = get_temp_journal_db(); + let mut db_result = get_temp_state_db(); let mut db = db_result.take(); - spec.ensure_db_good(db.as_hashdb_mut()).unwrap(); + spec.ensure_db_good(&mut db).unwrap(); let s = State::from_existing(db, genesis_header.state_root().clone(), engine.account_start_nonce(), Default::default()).unwrap(); assert_eq!(s.balance(&"0000000000000000000000000000000000000001".into()), 1u64.into()); assert_eq!(s.balance(&"0000000000000000000000000000000000000002".into()), 1u64.into()); diff --git a/ethcore/src/evm/tests.rs b/ethcore/src/evm/tests.rs index ec217b6c5..89a4c4ba9 100644 --- a/ethcore/src/evm/tests.rs +++ b/ethcore/src/evm/tests.rs @@ -141,7 +141,7 @@ impl Ext for FakeExt { } fn extcodesize(&self, address: &Address) -> usize { - self.codes.get(address).map(|v| v.len()).unwrap_or(0) + self.codes.get(address).map_or(0, |c| c.len()) } fn log(&mut self, topics: Vec, data: &[u8]) { diff --git a/ethcore/src/externalities.rs b/ethcore/src/externalities.rs index 7395522c3..82b946ffc 100644 --- a/ethcore/src/externalities.rs +++ b/ethcore/src/externalities.rs @@ -209,7 +209,6 @@ impl<'a, T, V> Ext for Externalities<'a, T, V> where T: 'a + Tracer, V: 'a + VMT self.state.code_size(address).unwrap_or(0) } - #[cfg_attr(feature="dev", allow(match_ref_pats))] fn ret(mut self, gas: &U256, data: &[u8]) -> evm::Result where Self: Sized { diff --git a/ethcore/src/lib.rs b/ethcore/src/lib.rs index 22b5a514a..cc398a2f5 100644 --- a/ethcore/src/lib.rs +++ b/ethcore/src/lib.rs @@ -110,6 +110,7 @@ extern crate lazy_static; extern crate heapsize; #[macro_use] extern crate ethcore_ipc as ipc; +extern crate lru_cache; #[cfg(feature = "jit" )] extern crate evmjit; @@ -142,6 +143,7 @@ mod basic_types; mod env_info; mod pod_account; mod state; +mod state_db; mod account_db; mod builtin; mod executive; diff --git a/ethcore/src/pod_account.rs b/ethcore/src/pod_account.rs index 703d61742..a92e03ebc 100644 --- a/ethcore/src/pod_account.rs +++ b/ethcore/src/pod_account.rs @@ -48,7 +48,7 @@ impl PodAccount { PodAccount { balance: *acc.balance(), nonce: *acc.nonce(), - storage: acc.storage_overlay().iter().fold(BTreeMap::new(), |mut m, (k, &(_, ref v))| {m.insert(k.clone(), v.clone()); m}), + storage: acc.storage_changes().iter().fold(BTreeMap::new(), |mut m, (k, v)| {m.insert(k.clone(), v.clone()); m}), code: acc.code().map(|x| x.to_vec()), } } diff --git a/ethcore/src/snapshot/account.rs b/ethcore/src/snapshot/account.rs index 8cfc4c96b..bc1faea3f 100644 --- a/ethcore/src/snapshot/account.rs +++ b/ethcore/src/snapshot/account.rs @@ -205,7 +205,7 @@ impl Account { #[cfg(test)] mod tests { use account_db::{AccountDB, AccountDBMut}; - use tests::helpers::get_temp_journal_db; + use tests::helpers::get_temp_state_db; use snapshot::tests::helpers::fill_storage; use util::sha3::{SHA3_EMPTY, SHA3_NULL_RLP}; @@ -218,8 +218,7 @@ mod tests { #[test] fn encoding_basic() { - let mut db = get_temp_journal_db(); - let mut db = &mut **db; + let mut db = get_temp_state_db(); let addr = Address::random(); let account = Account { @@ -239,8 +238,7 @@ mod tests { #[test] fn encoding_storage() { - let mut db = get_temp_journal_db(); - let mut db = &mut **db; + let mut db = get_temp_state_db(); let addr = Address::random(); let account = { @@ -265,8 +263,7 @@ mod tests { #[test] fn encoding_code() { - let mut db = get_temp_journal_db(); - let mut db = &mut **db; + let mut db = get_temp_state_db(); let addr1 = Address::random(); let addr2 = Address::random(); diff --git a/ethcore/src/spec/spec.rs b/ethcore/src/spec/spec.rs index 00676cef7..aeb50a0ef 100644 --- a/ethcore/src/spec/spec.rs +++ b/ethcore/src/spec/spec.rs @@ -20,6 +20,7 @@ use common::*; use engines::{Engine, NullEngine, InstantSeal, BasicAuthority}; use pod_state::*; use account_db::*; +use state_db::StateDB; use super::genesis::Genesis; use super::seal::Generic as GenericSeal; use ethereum; @@ -226,19 +227,19 @@ impl Spec { } /// Ensure that the given state DB has the trie nodes in for the genesis state. - pub fn ensure_db_good(&self, db: &mut HashDB) -> Result> { - if !db.contains(&self.state_root()) { + pub fn ensure_db_good(&self, db: &mut StateDB) -> Result> { + if !db.as_hashdb().contains(&self.state_root()) { let mut root = H256::new(); { - let mut t = SecTrieDBMut::new(db, &mut root); + let mut t = SecTrieDBMut::new(db.as_hashdb_mut(), &mut root); for (address, account) in self.genesis_state.get().iter() { try!(t.insert(&**address, &account.rlp())); } } for (address, account) in self.genesis_state.get().iter() { - account.insert_additional(&mut AccountDBMut::new(db, address)); + account.insert_additional(&mut AccountDBMut::new(db.as_hashdb_mut(), address)); } - assert!(db.contains(&self.state_root())); + assert!(db.as_hashdb().contains(&self.state_root())); Ok(true) } else { Ok(false) } } diff --git a/ethcore/src/state/account.rs b/ethcore/src/state/account.rs index cdd430290..07478220a 100644 --- a/ethcore/src/state/account.rs +++ b/ethcore/src/state/account.rs @@ -20,11 +20,13 @@ use std::collections::hash_map::Entry; use util::*; use pod_account::*; use rlp::*; +use lru_cache::LruCache; -use std::cell::{Ref, RefCell, Cell}; +use std::cell::{RefCell, Cell}; + +const STORAGE_CACHE_ITEMS: usize = 4096; /// Single account in the system. -#[derive(Clone)] pub struct Account { // Balance of the account. balance: U256, @@ -32,10 +34,16 @@ pub struct Account { nonce: U256, // Trie-backed storage. storage_root: H256, - // Overlay on trie-backed storage - tuple is (, ). - storage_overlay: RefCell>, + // LRU Cache of the trie-backed storage. + // This is limited to `STORAGE_CACHE_ITEMS` recent queries + storage_cache: RefCell>, + // Modified storage. Accumulates changes to storage made in `set_storage` + // Takes precedence over `storage_cache`. + storage_changes: HashMap, // Code hash of the account. If None, means that it's a contract whose code has not yet been set. code_hash: Option, + // Size of the accoun code. + code_size: Option, // Code cache of the account. code_cache: Bytes, // Account is new or has been modified @@ -52,23 +60,31 @@ impl Account { balance: balance, nonce: nonce, storage_root: SHA3_NULL_RLP, - storage_overlay: RefCell::new(storage.into_iter().map(|(k, v)| (k, (Filth::Dirty, v))).collect()), + storage_cache: Self::empty_storage_cache(), + storage_changes: storage, code_hash: Some(code.sha3()), + code_size: Some(code.len()), code_cache: code, filth: Filth::Dirty, address_hash: Cell::new(None), } } + fn empty_storage_cache() -> RefCell> { + RefCell::new(LruCache::new(STORAGE_CACHE_ITEMS)) + } + /// General constructor. pub fn from_pod(pod: PodAccount) -> Account { Account { balance: pod.balance, nonce: pod.nonce, storage_root: SHA3_NULL_RLP, - storage_overlay: RefCell::new(pod.storage.into_iter().map(|(k, v)| (k, (Filth::Dirty, v))).collect()), + storage_cache: Self::empty_storage_cache(), + storage_changes: pod.storage.into_iter().collect(), code_hash: pod.code.as_ref().map(|c| c.sha3()), - code_cache: pod.code.as_ref().map_or_else(|| { warn!("POD account with unknown code is being created! Assuming no code."); vec![] }, |c| c.clone()), + code_size: Some(pod.code.as_ref().map_or(0, |c| c.len())), + code_cache: pod.code.map_or_else(|| { warn!("POD account with unknown code is being created! Assuming no code."); vec![] }, |c| c), filth: Filth::Dirty, address_hash: Cell::new(None), } @@ -80,9 +96,11 @@ impl Account { balance: balance, nonce: nonce, storage_root: SHA3_NULL_RLP, - storage_overlay: RefCell::new(HashMap::new()), + storage_cache: Self::empty_storage_cache(), + storage_changes: HashMap::new(), code_hash: Some(SHA3_EMPTY), code_cache: vec![], + code_size: Some(0), filth: Filth::Dirty, address_hash: Cell::new(None), } @@ -95,9 +113,11 @@ impl Account { nonce: r.val_at(0), balance: r.val_at(1), storage_root: r.val_at(2), - storage_overlay: RefCell::new(HashMap::new()), + storage_cache: Self::empty_storage_cache(), + storage_changes: HashMap::new(), code_hash: Some(r.val_at(3)), code_cache: vec![], + code_size: None, filth: Filth::Clean, address_hash: Cell::new(None), } @@ -110,9 +130,11 @@ impl Account { balance: balance, nonce: nonce, storage_root: SHA3_NULL_RLP, - storage_overlay: RefCell::new(HashMap::new()), + storage_cache: Self::empty_storage_cache(), + storage_changes: HashMap::new(), code_hash: None, code_cache: vec![], + code_size: None, filth: Filth::Dirty, address_hash: Cell::new(None), } @@ -123,44 +145,62 @@ impl Account { pub fn init_code(&mut self, code: Bytes) { assert!(self.code_hash.is_none()); self.code_cache = code; + self.code_size = Some(self.code_cache.len()); self.filth = Filth::Dirty; } /// Reset this account's code to the given code. pub fn reset_code(&mut self, code: Bytes) { self.code_hash = None; + self.code_size = Some(0); self.init_code(code); } /// Set (and cache) the contents of the trie's storage at `key` to `value`. pub fn set_storage(&mut self, key: H256, value: H256) { - match self.storage_overlay.borrow_mut().entry(key) { - Entry::Occupied(ref mut entry) if entry.get().1 != value => { - entry.insert((Filth::Dirty, value)); + match self.storage_changes.entry(key) { + Entry::Occupied(ref mut entry) if entry.get() != &value => { + entry.insert(value); self.filth = Filth::Dirty; }, Entry::Vacant(entry) => { - entry.insert((Filth::Dirty, value)); + entry.insert(value); self.filth = Filth::Dirty; }, - _ => (), + _ => {}, } } /// Get (and cache) the contents of the trie's storage at `key`. + /// Takes modifed storage into account. pub fn storage_at(&self, db: &HashDB, key: &H256) -> H256 { - self.storage_overlay.borrow_mut().entry(key.clone()).or_insert_with(||{ - let db = SecTrieDB::new(db, &self.storage_root) - .expect("Account storage_root initially set to zero (valid) and only altered by SecTrieDBMut. \ - SecTrieDBMut would not set it to an invalid state root. Therefore the root is valid and DB creation \ - using it will not fail."); + if let Some(value) = self.cached_storage_at(key) { + return value; + } + let db = SecTrieDB::new(db, &self.storage_root) + .expect("Account storage_root initially set to zero (valid) and only altered by SecTrieDBMut. \ + SecTrieDBMut would not set it to an invalid state root. Therefore the root is valid and DB creation \ + using it will not fail."); - let item: U256 = match db.get(key){ - Ok(x) => x.map_or_else(U256::zero, decode), - Err(e) => panic!("Encountered potential DB corruption: {}", e), - }; - (Filth::Clean, item.into()) - }).1.clone() + let item: U256 = match db.get(key){ + Ok(x) => x.map_or_else(U256::zero, decode), + Err(e) => panic!("Encountered potential DB corruption: {}", e), + }; + let value: H256 = item.into(); + self.storage_cache.borrow_mut().insert(key.clone(), value.clone()); + value + } + + /// Get cached storage value if any. Returns `None` if the + /// key is not in the cache. + pub fn cached_storage_at(&self, key: &H256) -> Option { + if let Some(value) = self.storage_changes.get(key) { + return Some(value.clone()) + } + if let Some(value) = self.storage_cache.borrow_mut().get_mut(key) { + return Some(value.clone()) + } + None } /// return the balance associated with this account. @@ -196,6 +236,12 @@ impl Account { } } + /// returns the account's code size. If `None` then the code cache or code size cache isn't available - + /// get someone who knows to call `note_code`. + pub fn code_size(&self) -> Option { + self.code_size.clone() + } + #[cfg(test)] /// Provide a byte array which hashes to the `code_hash`. returns the hash as a result. pub fn note_code(&mut self, code: Bytes) -> Result<(), H256> { @@ -203,6 +249,7 @@ impl Account { match self.code_hash { Some(ref i) if h == *i => { self.code_cache = code; + self.code_size = Some(self.code_cache.len()); Ok(()) }, _ => Err(h) @@ -216,11 +263,12 @@ impl Account { /// Is this a new or modified account? pub fn is_dirty(&self) -> bool { - self.filth == Filth::Dirty + self.filth == Filth::Dirty || !self.storage_is_clean() } /// Mark account as clean. pub fn set_clean(&mut self) { + assert!(self.storage_is_clean()); self.filth = Filth::Clean } @@ -231,7 +279,31 @@ impl Account { self.is_cached() || match self.code_hash { Some(ref h) => match db.get(h) { - Some(x) => { self.code_cache = x.to_vec(); true }, + Some(x) => { + self.code_cache = x.to_vec(); + self.code_size = Some(x.len()); + true + }, + _ => { + warn!("Failed reverse get of {}", h); + false + }, + }, + _ => false, + } + } + + /// Provide a database to get `code_size`. Should not be called if it is a contract without code. + pub fn cache_code_size(&mut self, db: &HashDB) -> bool { + // TODO: fill out self.code_cache; + trace!("Account::cache_code_size: ic={}; self.code_hash={:?}, self.code_cache={}", self.is_cached(), self.code_hash, self.code_cache.pretty()); + self.code_size.is_some() || + match self.code_hash { + Some(ref h) if h != &SHA3_EMPTY => match db.get(h) { + Some(x) => { + self.code_size = Some(x.len()); + true + }, _ => { warn!("Failed reverse get of {}", h); false @@ -241,16 +313,15 @@ impl Account { } } - #[cfg(test)] /// Determine whether there are any un-`commit()`-ed storage-setting operations. - pub fn storage_is_clean(&self) -> bool { self.storage_overlay.borrow().iter().find(|&(_, &(f, _))| f == Filth::Dirty).is_none() } + pub fn storage_is_clean(&self) -> bool { self.storage_changes.is_empty() } #[cfg(test)] /// 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} } /// return the storage overlay. - pub fn storage_overlay(&self) -> Ref> { self.storage_overlay.borrow() } + pub fn storage_changes(&self) -> &HashMap { &self.storage_changes } /// Increment the nonce of the account by one. pub fn inc_nonce(&mut self) { @@ -276,26 +347,24 @@ impl Account { } } - /// Commit the `storage_overlay` to the backing DB and update `storage_root`. + /// Commit the `storage_changes` to the backing DB and update `storage_root`. pub fn commit_storage(&mut self, trie_factory: &TrieFactory, db: &mut HashDB) { let mut t = trie_factory.from_existing(db, &mut self.storage_root) .expect("Account storage_root initially set to zero (valid) and only altered by SecTrieDBMut. \ SecTrieDBMut would not set it to an invalid state root. Therefore the root is valid and DB creation \ using it will not fail."); - for (k, &mut (ref mut f, ref mut v)) in self.storage_overlay.borrow_mut().iter_mut() { - if f == &Filth::Dirty { - // cast key and value to trait type, - // so we can call overloaded `to_bytes` method - let res = match v.is_zero() { - true => t.remove(k), - false => t.insert(k, &encode(&U256::from(&*v))), - }; + for (k, v) in self.storage_changes.drain() { + // cast key and value to trait type, + // so we can call overloaded `to_bytes` method + let res = match v.is_zero() { + true => t.remove(&k), + false => t.insert(&k, &encode(&U256::from(&*v))), + }; - if let Err(e) = res { - warn!("Encountered potential DB corruption: {}", e); - } - *f = Filth::Clean; + if let Err(e) = res { + warn!("Encountered potential DB corruption: {}", e); } + self.storage_cache.borrow_mut().insert(k, v); } } @@ -303,9 +372,13 @@ impl Account { pub fn commit_code(&mut self, db: &mut HashDB) { trace!("Commiting code of {:?} - {:?}, {:?}", self, self.code_hash.is_none(), self.code_cache.is_empty()); match (self.code_hash.is_none(), self.code_cache.is_empty()) { - (true, true) => self.code_hash = Some(SHA3_EMPTY), + (true, true) => { + self.code_hash = Some(SHA3_EMPTY); + self.code_size = Some(0); + }, (true, false) => { self.code_hash = Some(db.insert(&self.code_cache)); + self.code_size = Some(self.code_cache.len()); }, (false, _) => {}, } @@ -317,9 +390,57 @@ impl Account { stream.append(&self.nonce); stream.append(&self.balance); stream.append(&self.storage_root); - stream.append(self.code_hash.as_ref().expect("Cannot form RLP of contract account without code.")); + stream.append(self.code_hash.as_ref().unwrap_or(&SHA3_EMPTY)); stream.out() } + + /// Clone basic account data + pub fn clone_basic(&self) -> Account { + Account { + balance: self.balance.clone(), + nonce: self.nonce.clone(), + storage_root: self.storage_root.clone(), + storage_cache: Self::empty_storage_cache(), + storage_changes: HashMap::new(), + code_hash: self.code_hash.clone(), + code_size: self.code_size.clone(), + code_cache: Bytes::new(), + filth: self.filth, + address_hash: self.address_hash.clone(), + } + } + + /// Clone account data and dirty storage keys + pub fn clone_dirty(&self) -> Account { + let mut account = self.clone_basic(); + account.storage_changes = self.storage_changes.clone(); + account.code_cache = self.code_cache.clone(); + account + } + + /// Clone account data, dirty storage keys and cached storage keys. + pub fn clone_all(&self) -> Account { + let mut account = self.clone_dirty(); + account.storage_cache = self.storage_cache.clone(); + account + } + + /// Replace self with the data from other account merging storage cache + pub fn merge_with(&mut self, other: Account) { + assert!(self.storage_is_clean()); + assert!(other.storage_is_clean()); + self.balance = other.balance; + self.nonce = other.nonce; + self.storage_root = other.storage_root; + self.code_hash = other.code_hash; + self.code_cache = other.code_cache; + self.code_size = other.code_size; + self.address_hash = other.address_hash; + let mut cache = self.storage_cache.borrow_mut(); + for (k, v) in other.storage_cache.into_inner().into_iter() { + cache.insert(k.clone() , v.clone()); //TODO: cloning should not be required here + } + } } impl fmt::Debug for Account { @@ -416,6 +537,7 @@ mod tests { let mut db = AccountDBMut::new(&mut db, &Address::new()); a.init_code(vec![0x55, 0x44, 0xffu8]); assert_eq!(a.code_hash(), SHA3_EMPTY); + assert_eq!(a.code_size(), Some(3)); a.commit_code(&mut db); assert_eq!(a.code_hash().hex(), "af231e631776a517ca23125370d542873eca1fb4d613ed9b5d5335a46ae5b7eb"); } diff --git a/ethcore/src/state/mod.rs b/ethcore/src/state/mod.rs index e1299d1dc..79d7cba54 100644 --- a/ethcore/src/state/mod.rs +++ b/ethcore/src/state/mod.rs @@ -23,6 +23,7 @@ use trace::FlatTrace; use pod_account::*; use pod_state::{self, PodState}; use types::state_diff::StateDiff; +use state_db::StateDB; mod account; mod substate; @@ -41,23 +42,92 @@ pub struct ApplyOutcome { /// Result type for the execution ("application") of a transaction. pub type ApplyResult = Result; +#[derive(Debug)] +enum AccountEntry { + /// Contains account data. + Cached(Account), + /// Account has been deleted. + Killed, + /// Account does not exist. + Missing, +} + +impl AccountEntry { + fn is_dirty(&self) -> bool { + match *self { + AccountEntry::Cached(ref a) => a.is_dirty(), + AccountEntry::Killed => true, + AccountEntry::Missing => false, + } + } + + /// Clone dirty data into new `AccountEntry`. + /// Returns None if clean. + fn clone_dirty(&self) -> Option { + match *self { + AccountEntry::Cached(ref acc) if acc.is_dirty() => Some(AccountEntry::Cached(acc.clone_dirty())), + AccountEntry::Killed => Some(AccountEntry::Killed), + _ => None, + } + } + + /// Clone account entry data that needs to be saved in the snapshot. + /// This includes basic account information and all locally cached storage keys + fn clone_for_snapshot(&self) -> AccountEntry { + match *self { + AccountEntry::Cached(ref acc) => AccountEntry::Cached(acc.clone_all()), + AccountEntry::Killed => AccountEntry::Killed, + AccountEntry::Missing => AccountEntry::Missing, + } + } +} + /// Representation of the entire state of all accounts in the system. +/// +/// `State` can work together with `StateDB` to share account cache. +/// +/// Local cache contains changes made locally and changes accumulated +/// locally from previous commits. Global cache reflects the database +/// state and never contains any changes. +/// +/// Account data can be in the following cache states: +/// * In global but not local - something that was queried from the database, +/// but never modified +/// * In local but not global - something that was just added (e.g. new account) +/// * In both with the same value - something that was changed to a new value, +/// but changed back to a previous block in the same block (same State instance) +/// * In both with different values - something that was overwritten with a +/// new value. +/// +/// All read-only state queries check local cache/modifications first, +/// then global state cache. If data is not found in any of the caches +/// it is loaded from the DB to the local cache. +/// +/// Upon destruction all the local cache data merged into the global cache. +/// The merge might be rejected if current state is non-canonical. pub struct State { - db: Box, + db: StateDB, root: H256, - cache: RefCell>>, - snapshots: RefCell>>>>, + cache: RefCell>, + snapshots: RefCell>>>, account_start_nonce: U256, factories: Factories, } +#[derive(Copy, Clone)] +enum RequireCache { + None, + CodeSize, + Code, +} + 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. \ Therefore creating a SecTrieDB with this state's root will not fail."; impl State { /// Creates new state with empty state root #[cfg(test)] - pub fn new(mut db: Box, account_start_nonce: U256, factories: Factories) -> State { + pub fn new(mut db: StateDB, account_start_nonce: U256, factories: Factories) -> State { let mut root = H256::new(); { // init trie and reset root too null @@ -75,7 +145,7 @@ impl State { } /// Creates new state with existing state root - pub fn from_existing(db: Box, root: H256, account_start_nonce: U256, factories: Factories) -> Result { + pub fn from_existing(db: StateDB, root: H256, account_start_nonce: U256, factories: Factories) -> Result { if !db.as_hashdb().contains(&root) { return Err(TrieError::InvalidStateRoot(root)); } @@ -119,14 +189,21 @@ impl State { self.cache.borrow_mut().insert(k, v); }, None => { - self.cache.borrow_mut().remove(&k); + match self.cache.borrow_mut().entry(k) { + ::std::collections::hash_map::Entry::Occupied(e) => { + if e.get().is_dirty() { + e.remove(); + } + }, + _ => {} + } } } } } } - fn insert_cache(&self, address: &Address, account: Option) { + fn insert_cache(&self, address: &Address, account: AccountEntry) { if let Some(ref mut snapshot) = self.snapshots.borrow_mut().last_mut() { if !snapshot.contains_key(address) { snapshot.insert(address.clone(), self.cache.borrow_mut().insert(address.clone(), account)); @@ -139,13 +216,14 @@ impl State { fn note_cache(&self, address: &Address) { if let Some(ref mut snapshot) = self.snapshots.borrow_mut().last_mut() { if !snapshot.contains_key(address) { - snapshot.insert(address.clone(), self.cache.borrow().get(address).cloned()); + snapshot.insert(address.clone(), self.cache.borrow().get(address).map(AccountEntry::clone_for_snapshot)); } } } /// Destroy the current object and return root and database. - pub fn drop(self) -> (H256, Box) { + pub fn drop(mut self) -> (H256, StateDB) { + self.commit_cache(); (self.root, self.db) } @@ -157,50 +235,99 @@ impl State { /// Create a new contract at address `contract`. If there is already an account at the address /// it will have its code reset, ready for `init_code()`. pub fn new_contract(&mut self, contract: &Address, balance: U256) { - self.insert_cache(contract, Some(Account::new_contract(balance, self.account_start_nonce))); + self.insert_cache(contract, AccountEntry::Cached(Account::new_contract(balance, self.account_start_nonce))); } /// Remove an existing account. pub fn kill_account(&mut self, account: &Address) { - self.insert_cache(account, None); + self.insert_cache(account, AccountEntry::Killed); } /// Determine whether an account exists. pub fn exists(&self, a: &Address) -> bool { - self.ensure_cached(a, false, |a| a.is_some()) + self.ensure_cached(a, RequireCache::None, |a| a.is_some()) } /// Get the balance of account `a`. pub fn balance(&self, a: &Address) -> U256 { - self.ensure_cached(a, false, + self.ensure_cached(a, RequireCache::None, |a| a.as_ref().map_or(U256::zero(), |account| *account.balance())) } /// Get the nonce of account `a`. pub fn nonce(&self, a: &Address) -> U256 { - self.ensure_cached(a, false, + self.ensure_cached(a, RequireCache::None, |a| a.as_ref().map_or(self.account_start_nonce, |account| *account.nonce())) } /// Mutate storage of account `address` so that it is `value` for `key`. pub fn storage_at(&self, address: &Address, key: &H256) -> H256 { - self.ensure_cached(address, false, |a| a.as_ref().map_or(H256::new(), |a| { - let addr_hash = a.address_hash(address); - let db = self.factories.accountdb.readonly(self.db.as_hashdb(), addr_hash); - a.storage_at(db.as_hashdb(), key) - })) + // Storage key search and update works like this: + // 1. If there's an entry for the account in the local cache check for the key and return it if found. + // 2. If there's an entry for the account in the global cache check for the key or load it into that account. + // 3. If account is missing in the global cache load it into the local cache and cache the key there. + + // check local cache first without updating + { + let local_cache = self.cache.borrow_mut(); + let mut local_account = None; + if let Some(maybe_acc) = local_cache.get(address) { + match *maybe_acc { + AccountEntry::Cached(ref account) => { + if let Some(value) = account.cached_storage_at(key) { + return value; + } else { + local_account = Some(maybe_acc); + } + }, + _ => return H256::new(), + } + } + // check the global cache and and cache storage key there if found, + // otherwise cache the account localy and cache storage key there. + if let Some(result) = self.db.get_cached(address, |acc| acc.map_or(H256::new(), |a| { + let account_db = self.factories.accountdb.readonly(self.db.as_hashdb(), a.address_hash(address)); + a.storage_at(account_db.as_hashdb(), key) + })) { + return result; + } + if let Some(ref mut acc) = local_account { + if let AccountEntry::Cached(ref account) = **acc { + let account_db = self.factories.accountdb.readonly(self.db.as_hashdb(), account.address_hash(address)); + return account.storage_at(account_db.as_hashdb(), key) + } else { + return H256::new() + } + } + } + + // account is not found in the global cache, get from the DB and insert into local + let db = self.factories.trie.readonly(self.db.as_hashdb(), &self.root).expect(SEC_TRIE_DB_UNWRAP_STR); + let maybe_acc = match db.get(address) { + Ok(acc) => acc.map(Account::from_rlp), + Err(e) => panic!("Potential DB corruption encountered: {}", e), + }; + let r = maybe_acc.as_ref().map_or(H256::new(), |a| { + let account_db = self.factories.accountdb.readonly(self.db.as_hashdb(), a.address_hash(address)); + a.storage_at(account_db.as_hashdb(), key) + }); + match maybe_acc { + Some(account) => self.insert_cache(address, AccountEntry::Cached(account)), + None => self.insert_cache(address, AccountEntry::Missing), + } + r } - /// Get the code of account `a`. + /// Get accounts' code. pub fn code(&self, a: &Address) -> Option { - self.ensure_cached(a, true, - |a| a.as_ref().map_or(None, |a| a.code().map(|x| x.to_vec()))) + self.ensure_cached(a, RequireCache::Code, + |a| a.as_ref().map_or(None, |a| a.code().map(|x|x.to_vec()))) } - /// Get the code size of account `a`. + /// Get accounts' code size. pub fn code_size(&self, a: &Address) -> Option { - self.ensure_cached(a, true, - |a| a.as_ref().map_or(None, |a| a.code().map(|x| x.len()))) + self.ensure_cached(a, RequireCache::CodeSize, + |a| a.as_ref().and_then(|a| a.code_size())) } /// Add `incr` to the balance of account `a`. @@ -262,19 +389,19 @@ impl State { /// 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. #[cfg_attr(feature="dev", allow(match_ref_pats))] - pub fn commit_into( + fn commit_into( factories: &Factories, - db: &mut HashDB, + db: &mut StateDB, root: &mut H256, - accounts: &mut HashMap> + accounts: &mut HashMap ) -> Result<(), Error> { // first, commit the sub trees. // TODO: is this necessary or can we dispense with the `ref mut a` for just `a`? for (address, ref mut a) in accounts.iter_mut() { match a { - &mut&mut Some(ref mut account) if account.is_dirty() => { + &mut&mut AccountEntry::Cached(ref mut account) if account.is_dirty() => { let addr_hash = account.address_hash(address); - let mut account_db = factories.accountdb.create(db, addr_hash); + let mut account_db = factories.accountdb.create(db.as_hashdb_mut(), addr_hash); account.commit_storage(&factories.trie, account_db.as_hashdb_mut()); account.commit_code(account_db.as_hashdb_mut()); } @@ -283,15 +410,18 @@ impl State { } { - let mut trie = factories.trie.from_existing(db, root).unwrap(); + let mut trie = factories.trie.from_existing(db.as_hashdb_mut(), root).unwrap(); for (address, ref mut a) in accounts.iter_mut() { match **a { - Some(ref mut account) if account.is_dirty() => { + AccountEntry::Cached(ref mut account) if account.is_dirty() => { account.set_clean(); - try!(trie.insert(address, &account.rlp())) + try!(trie.insert(address, &account.rlp())); }, - None => try!(trie.remove(address)), - _ => (), + AccountEntry::Killed => { + try!(trie.remove(address)); + **a = AccountEntry::Missing; + }, + _ => {}, } } } @@ -299,10 +429,27 @@ impl State { Ok(()) } + fn commit_cache(&mut self) { + let mut addresses = self.cache.borrow_mut(); + for (address, a) in addresses.drain() { + match a { + AccountEntry::Cached(account) => { + if !account.is_dirty() { + self.db.cache_account(address, Some(account)); + } + }, + AccountEntry::Missing => { + self.db.cache_account(address, None); + }, + _ => {}, + } + } + } + /// Commits our cached account changes into the trie. pub fn commit(&mut self) -> Result<(), Error> { assert!(self.snapshots.borrow().is_empty()); - Self::commit_into(&self.factories, self.db.as_hashdb_mut(), &mut self.root, &mut *self.cache.borrow_mut()) + Self::commit_into(&self.factories, &mut self.db, &mut self.root, &mut *self.cache.borrow_mut()) } /// Clear state cache @@ -316,7 +463,7 @@ impl State { pub fn populate_from(&mut self, accounts: PodState) { assert!(self.snapshots.borrow().is_empty()); for (add, acc) in accounts.drain().into_iter() { - self.cache.borrow_mut().insert(add, Some(Account::from_pod(acc))); + self.cache.borrow_mut().insert(add, AccountEntry::Cached(Account::from_pod(acc))); } } @@ -326,7 +473,7 @@ impl State { // TODO: handle database rather than just the cache. // will need fat db. PodState::from(self.cache.borrow().iter().fold(BTreeMap::new(), |mut m, (add, opt)| { - if let Some(ref acc) = *opt { + if let AccountEntry::Cached(ref acc) = *opt { m.insert(add.clone(), PodAccount::from_account(acc)); } m @@ -335,7 +482,7 @@ impl State { fn query_pod(&mut self, query: &PodState) { for (address, pod_account) in query.get() { - self.ensure_cached(address, true, |a| { + self.ensure_cached(address, RequireCache::Code, |a| { if a.is_some() { for key in pod_account.storage.keys() { self.storage_at(address, key); @@ -354,28 +501,61 @@ impl State { pod_state::diff_pod(&state_pre.to_pod(), &pod_state_post) } - /// Ensure account `a` is in our cache of the trie DB and return a handle for getting it. - /// `require_code` requires that the code be cached, too. - fn ensure_cached<'a, F, U>(&'a self, a: &'a Address, require_code: bool, f: F) -> U - where F: FnOnce(&Option) -> U { - let have_key = self.cache.borrow().contains_key(a); - if !have_key { - let db = self.factories.trie.readonly(self.db.as_hashdb(), &self.root).expect(SEC_TRIE_DB_UNWRAP_STR); - let maybe_acc = match db.get(a) { - Ok(acc) => acc.map(Account::from_rlp), - Err(e) => panic!("Potential DB corruption encountered: {}", e), - }; - self.insert_cache(a, maybe_acc); - } - if require_code { - if let Some(ref mut account) = self.cache.borrow_mut().get_mut(a).unwrap().as_mut() { - let addr_hash = account.address_hash(a); - let accountdb = self.factories.accountdb.readonly(self.db.as_hashdb(), addr_hash); - account.cache_code(accountdb.as_hashdb()); + fn update_account_cache(require: RequireCache, account: &mut Account, db: &HashDB) { + match require { + RequireCache::None => {}, + RequireCache::Code => { + account.cache_code(db); + } + RequireCache::CodeSize => { + account.cache_code_size(db); } } + } - f(self.cache.borrow().get(a).unwrap()) + /// Check caches for required data + /// First searches for account in the local, then the shared cache. + /// Populates local cache if nothing found. + fn ensure_cached(&self, a: &Address, require: RequireCache, f: F) -> U + where F: Fn(Option<&Account>) -> U { + // check local cache first + if let Some(ref mut maybe_acc) = self.cache.borrow_mut().get_mut(a) { + if let AccountEntry::Cached(ref mut account) = **maybe_acc { + let accountdb = self.factories.accountdb.readonly(self.db.as_hashdb(), account.address_hash(a)); + Self::update_account_cache(require, account, accountdb.as_hashdb()); + return f(Some(account)); + } + return f(None); + } + // check global cache + let result = self.db.get_cached(a, |mut acc| { + if let Some(ref mut account) = acc { + let accountdb = self.factories.accountdb.readonly(self.db.as_hashdb(), account.address_hash(a)); + Self::update_account_cache(require, account, accountdb.as_hashdb()); + } + f(acc.map(|a| &*a)) + }); + match result { + Some(r) => r, + None => { + // not found in the global cache, get from the DB and insert into local + let db = self.factories.trie.readonly(self.db.as_hashdb(), &self.root).expect(SEC_TRIE_DB_UNWRAP_STR); + let mut maybe_acc = match db.get(a) { + Ok(acc) => acc.map(Account::from_rlp), + Err(e) => panic!("Potential DB corruption encountered: {}", e), + }; + if let Some(ref mut account) = maybe_acc.as_mut() { + let accountdb = self.factories.accountdb.readonly(self.db.as_hashdb(), account.address_hash(a)); + Self::update_account_cache(require, account, accountdb.as_hashdb()); + } + let r = f(maybe_acc.as_ref()); + match maybe_acc { + Some(account) => self.insert_cache(a, AccountEntry::Cached(account)), + None => self.insert_cache(a, AccountEntry::Missing), + } + r + } + } } /// Pull account `a` in our cache from the trie DB. `require_code` requires that the code be cached, too. @@ -390,30 +570,40 @@ impl State { { let contains_key = self.cache.borrow().contains_key(a); if !contains_key { - let db = self.factories.trie.readonly(self.db.as_hashdb(), &self.root).expect(SEC_TRIE_DB_UNWRAP_STR); - let maybe_acc = match db.get(a) { - Ok(acc) => acc.map(Account::from_rlp), - Err(e) => panic!("Potential DB corruption encountered: {}", e), - }; - - self.insert_cache(a, maybe_acc); + match self.db.get_cached_account(a) { + Some(Some(acc)) => self.insert_cache(a, AccountEntry::Cached(acc)), + Some(None) => self.insert_cache(a, AccountEntry::Missing), + None => { + let db = self.factories.trie.readonly(self.db.as_hashdb(), &self.root).expect(SEC_TRIE_DB_UNWRAP_STR); + let maybe_acc = match db.get(a) { + Ok(Some(acc)) => AccountEntry::Cached(Account::from_rlp(acc)), + Ok(None) => AccountEntry::Missing, + Err(e) => panic!("Potential DB corruption encountered: {}", e), + }; + self.insert_cache(a, maybe_acc); + } + } } else { self.note_cache(a); } match self.cache.borrow_mut().get_mut(a).unwrap() { - &mut Some(ref mut acc) => not_default(acc), - slot @ &mut None => *slot = Some(default()), + &mut AccountEntry::Cached(ref mut acc) => not_default(acc), + slot => *slot = AccountEntry::Cached(default()), } RefMut::map(self.cache.borrow_mut(), |c| { - let account = c.get_mut(a).unwrap().as_mut().unwrap(); - if require_code { - let addr_hash = account.address_hash(a); - let accountdb = self.factories.accountdb.readonly(self.db.as_hashdb(), addr_hash); - account.cache_code(accountdb.as_hashdb()); + match c.get_mut(a).unwrap() { + &mut AccountEntry::Cached(ref mut account) => { + if require_code { + let addr_hash = account.address_hash(a); + let accountdb = self.factories.accountdb.readonly(self.db.as_hashdb(), addr_hash); + account.cache_code(accountdb.as_hashdb()); + } + account + }, + _ => panic!("Required account must always exist; qed"), } - account }) } } @@ -427,17 +617,10 @@ impl fmt::Debug for State { impl Clone for State { fn clone(&self) -> State { let cache = { - let mut cache = HashMap::new(); + let mut cache: HashMap = HashMap::new(); for (key, val) in self.cache.borrow().iter() { - let key = key.clone(); - match *val { - Some(ref acc) if acc.is_dirty() => { - cache.insert(key, Some(acc.clone())); - }, - None => { - cache.insert(key, None); - }, - _ => {}, + if let Some(entry) = val.clone_dirty() { + cache.insert(key.clone(), entry); } } cache @@ -447,7 +630,7 @@ impl Clone for State { db: self.db.boxed_clone(), root: self.root.clone(), cache: RefCell::new(cache), - snapshots: RefCell::new(self.snapshots.borrow().clone()), + snapshots: RefCell::new(Vec::new()), account_start_nonce: self.account_start_nonce.clone(), factories: self.factories.clone(), } diff --git a/ethcore/src/state_db.rs b/ethcore/src/state_db.rs new file mode 100644 index 000000000..af6780cdc --- /dev/null +++ b/ethcore/src/state_db.rs @@ -0,0 +1,161 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +use lru_cache::LruCache; +use util::journaldb::JournalDB; +use util::hash::{H256}; +use util::hashdb::HashDB; +use util::{Arc, Address, DBTransaction, UtilError, Mutex}; +use state::Account; + +const STATE_CACHE_ITEMS: usize = 65536; + +struct AccountCache { + /// DB Account cache. `None` indicates that account is known to be missing. + accounts: LruCache>, +} + +/// State database abstraction. +/// Manages shared global state cache. +/// A clone of `StateDB` may be created as canonical or not. +/// For canonical clones cache changes are accumulated and applied +/// on commit. +/// For non-canonical clones cache is cleared on commit. +pub struct StateDB { + db: Box, + account_cache: Arc>, + cache_overlay: Vec<(Address, Option)>, + is_canon: bool, +} + +impl StateDB { + /// Create a new instance wrapping `JournalDB` + pub fn new(db: Box) -> StateDB { + StateDB { + db: db, + account_cache: Arc::new(Mutex::new(AccountCache { accounts: LruCache::new(STATE_CACHE_ITEMS) })), + cache_overlay: Vec::new(), + is_canon: false, + } + } + + /// Commit all recent insert operations and canonical historical commits' removals from the + /// old era to the backing database, reverting any non-canonical historical commit's inserts. + pub fn commit(&mut self, batch: &mut DBTransaction, now: u64, id: &H256, end: Option<(u64, H256)>) -> Result { + let records = try!(self.db.commit(batch, now, id, end)); + if self.is_canon { + self.commit_cache(); + } else { + self.clear_cache(); + } + Ok(records) + } + + /// Returns an interface to HashDB. + pub fn as_hashdb(&self) -> &HashDB { + self.db.as_hashdb() + } + + /// Returns an interface to mutable HashDB. + pub fn as_hashdb_mut(&mut self) -> &mut HashDB { + self.db.as_hashdb_mut() + } + + /// Clone the database. + pub fn boxed_clone(&self) -> StateDB { + StateDB { + db: self.db.boxed_clone(), + account_cache: self.account_cache.clone(), + cache_overlay: Vec::new(), + is_canon: false, + } + } + + /// Clone the database for a canonical state. + pub fn boxed_clone_canon(&self) -> StateDB { + StateDB { + db: self.db.boxed_clone(), + account_cache: self.account_cache.clone(), + cache_overlay: Vec::new(), + is_canon: true, + } + } + + /// Check if pruning is enabled on the database. + pub fn is_pruned(&self) -> bool { + self.db.is_pruned() + } + + /// Heap size used. + pub fn mem_used(&self) -> usize { + self.db.mem_used() //TODO: + self.account_cache.lock().heap_size_of_children() + } + + /// Returns underlying `JournalDB`. + pub fn journal_db(&self) -> &JournalDB { + &*self.db + } + + /// Enqueue cache change. + pub fn cache_account(&mut self, addr: Address, data: Option) { + self.cache_overlay.push((addr, data)); + } + + /// Apply pending cache changes. + fn commit_cache(&mut self) { + let mut cache = self.account_cache.lock(); + for (address, account) in self.cache_overlay.drain(..) { + if let Some(&mut Some(ref mut existing)) = cache.accounts.get_mut(&address) { + if let Some(new) = account { + existing.merge_with(new); + continue; + } + } + cache.accounts.insert(address, account); + } + } + + /// Clear the cache. + pub fn clear_cache(&mut self) { + self.cache_overlay.clear(); + let mut cache = self.account_cache.lock(); + cache.accounts.clear(); + } + + /// Get basic copy of the cached account. Does not include storage. + /// Returns 'None' if the state is non-canonical and cache is disabled + /// or if the account is not cached. + pub fn get_cached_account(&self, addr: &Address) -> Option> { + if !self.is_canon { + return None; + } + let mut cache = self.account_cache.lock(); + cache.accounts.get_mut(&addr).map(|a| a.as_ref().map(|a| a.clone_basic())) + } + + /// Get value from a cached account. + /// Returns 'None' if the state is non-canonical and cache is disabled + /// or if the account is not cached. + pub fn get_cached(&self, a: &Address, f: F) -> Option + where F: FnOnce(Option<&mut Account>) -> U { + if !self.is_canon { + return None; + } + let mut cache = self.account_cache.lock(); + cache.accounts.get_mut(a).map(|c| f(c.as_mut())) + } +} + diff --git a/ethcore/src/tests/helpers.rs b/ethcore/src/tests/helpers.rs index c1f99f434..6504ef8a9 100644 --- a/ethcore/src/tests/helpers.rs +++ b/ethcore/src/tests/helpers.rs @@ -19,6 +19,7 @@ use io::*; use client::{BlockChainClient, Client, ClientConfig}; use common::*; use spec::*; +use state_db::StateDB; use block::{OpenBlock, Drain}; use blockchain::{BlockChain, Config as BlockChainConfig}; use state::*; @@ -146,9 +147,9 @@ pub fn generate_dummy_client_with_spec_and_data(get_test_spec: F, block_numbe ).unwrap(); let test_engine = &*test_spec.engine; - let mut db_result = get_temp_journal_db(); + let mut db_result = get_temp_state_db(); let mut db = db_result.take(); - test_spec.ensure_db_good(db.as_hashdb_mut()).unwrap(); + test_spec.ensure_db_good(&mut db).unwrap(); let genesis_header = test_spec.genesis_header(); let mut rolling_timestamp = 40; @@ -321,9 +322,9 @@ pub fn generate_dummy_empty_blockchain() -> GuardedTempResult { } } -pub fn get_temp_journal_db() -> GuardedTempResult> { +pub fn get_temp_state_db() -> GuardedTempResult { let temp = RandomTempPath::new(); - let journal_db = get_temp_journal_db_in(temp.as_path()); + let journal_db = get_temp_state_db_in(temp.as_path()); GuardedTempResult { _temp: temp, @@ -333,7 +334,7 @@ pub fn get_temp_journal_db() -> GuardedTempResult> { pub fn get_temp_state() -> GuardedTempResult { let temp = RandomTempPath::new(); - let journal_db = get_temp_journal_db_in(temp.as_path()); + let journal_db = get_temp_state_db_in(temp.as_path()); GuardedTempResult { _temp: temp, @@ -341,13 +342,14 @@ pub fn get_temp_state() -> GuardedTempResult { } } -pub fn get_temp_journal_db_in(path: &Path) -> Box { +pub fn get_temp_state_db_in(path: &Path) -> StateDB { let db = new_db(path.to_str().expect("Only valid utf8 paths for tests.")); - journaldb::new(db.clone(), journaldb::Algorithm::EarlyMerge, None) + let journal_db = journaldb::new(db.clone(), journaldb::Algorithm::EarlyMerge, None); + StateDB::new(journal_db) } pub fn get_temp_state_in(path: &Path) -> State { - let journal_db = get_temp_journal_db_in(path); + let journal_db = get_temp_state_db_in(path); State::new(journal_db, U256::from(0), Default::default()) } diff --git a/logger/src/lib.rs b/logger/src/lib.rs index e672a3e28..79655d2f6 100644 --- a/logger/src/lib.rs +++ b/logger/src/lib.rs @@ -91,10 +91,10 @@ pub fn setup_log(config: &Config) -> Result, String> { let timestamp = time::strftime("%Y-%m-%d %H:%M:%S %Z", &time::now()).unwrap(); let with_color = if max_log_level() <= LogLevelFilter::Info { - format!("{}{}", Colour::Black.bold().paint(timestamp), record.args()) + format!("{} {}", Colour::Black.bold().paint(timestamp), record.args()) } else { let name = thread::current().name().map_or_else(Default::default, |x| format!("{}", Colour::Blue.bold().paint(x))); - format!("{}{} {} {} {}", Colour::Black.bold().paint(timestamp), name, record.level(), record.target(), record.args()) + format!("{} {} {} {} {}", Colour::Black.bold().paint(timestamp), name, record.level(), record.target(), record.args()) }; let removed_color = kill_color(with_color.as_ref());