diff --git a/Cargo.lock b/Cargo.lock index c79806e732..bf7b0c295c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -461,6 +461,8 @@ dependencies = [ "primitives", "rand 0.6.1", "rlp", + "rlp_derive", + "snap", "tempfile", "trie-standardmap", ] diff --git a/codechain/codechain.yml b/codechain/codechain.yml index 0558880d58..ff0964615c 100644 --- a/codechain/codechain.yml +++ b/codechain/codechain.yml @@ -260,6 +260,16 @@ args: takes_value: true conflicts_with: - no-discovery + - snapshot-hash: + long: snapshot-hash + value_name: HASH + requires: snapshot-number + takes_value: true + - snapshot-number: + long: snapshot-number + value_name: NUM + requires: snapshot-hash + takes_value: true - no-snapshot: long: no-snapshot help: Disable snapshots diff --git a/codechain/config/mod.rs b/codechain/config/mod.rs index e4ea5538d6..e5dac23f3b 100644 --- a/codechain/config/mod.rs +++ b/codechain/config/mod.rs @@ -25,6 +25,7 @@ use cidr::IpCidr; use ckey::PlatformAddress; use clap; use cnetwork::{FilterEntry, NetworkConfig, SocketAddr}; +use primitives::H256; use toml; pub use self::chain_type::ChainType; @@ -274,6 +275,8 @@ pub struct Network { pub min_peers: Option<usize>, pub max_peers: Option<usize>, pub sync: Option<bool>, + pub snapshot_hash: Option<H256>, + pub snapshot_number: Option<u64>, pub transaction_relay: Option<bool>, pub discovery: Option<bool>, pub discovery_type: Option<String>, @@ -313,6 +316,8 @@ fn default_enable_devel_api() -> bool { pub struct Snapshot { pub disable: Option<bool>, pub path: Option<String>, + // Snapshot's age in blocks + pub expiration: Option<u64>, } #[derive(Deserialize)] @@ -575,6 +580,12 @@ impl Network { if other.sync.is_some() { self.sync = other.sync; } + if other.snapshot_hash.is_some() { + self.snapshot_hash = other.snapshot_hash; + } + if other.snapshot_number.is_some() { + self.snapshot_number = other.snapshot_number; + } if other.transaction_relay.is_some() { self.transaction_relay = other.transaction_relay; } @@ -627,6 +638,12 @@ impl Network { if matches.is_present("no-sync") { self.sync = Some(false); } + if let Some(snapshot_hash) = matches.value_of("snapshot-hash") { + self.snapshot_hash = Some(snapshot_hash.parse().map_err(|_| "Invalid snapshot-hash")?); + } + if let Some(snapshot_number) = matches.value_of("snapshot-number") { + self.snapshot_number = Some(snapshot_number.parse().map_err(|_| "Invalid snapshot-number")?); + } if matches.is_present("no-tx-relay") { self.transaction_relay = Some(false); } @@ -739,6 +756,9 @@ impl Snapshot { if other.path.is_some() { self.path = other.path.clone(); } + if other.expiration.is_some() { + self.expiration = other.expiration; + } } pub fn overwrite_with(&mut self, matches: &clap::ArgMatches) -> Result<(), String> { diff --git a/codechain/config/presets/config.dev.toml b/codechain/config/presets/config.dev.toml index 2b8890b4f7..279704592a 100644 --- a/codechain/config/presets/config.dev.toml +++ b/codechain/config/presets/config.dev.toml @@ -52,6 +52,7 @@ max_connections = 100 [snapshot] disable = false path = "snapshot" +expiration = 100000 # blocks. About a week [stratum] disable = false diff --git a/codechain/config/presets/config.prod.toml b/codechain/config/presets/config.prod.toml index b67e2746bb..013883673d 100644 --- a/codechain/config/presets/config.prod.toml +++ b/codechain/config/presets/config.prod.toml @@ -52,6 +52,7 @@ max_connections = 100 [snapshot] disable = true path = "snapshot" +expiration = 100000 # blocks. About a week [stratum] disable = true diff --git a/codechain/rpc.rs b/codechain/rpc.rs index 5a3f696ca4..b3219e25d8 100644 --- a/codechain/rpc.rs +++ b/codechain/rpc.rs @@ -15,8 +15,8 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. use std::io; -use std::net::SocketAddr; +use crate::config::Config; use crate::rpc_apis; use crpc::{ jsonrpc_core, start_http, start_ipc, start_ws, HttpServer, IpcServer, MetaIoHandler, Middleware, WsError, WsServer, @@ -33,38 +33,27 @@ pub struct RpcHttpConfig { } pub fn rpc_http_start( - cfg: RpcHttpConfig, - enable_devel_api: bool, - deps: &rpc_apis::ApiDependencies, + server: MetaIoHandler<(), impl Middleware<()>>, + config: RpcHttpConfig, ) -> Result<HttpServer, String> { - let url = format!("{}:{}", cfg.interface, cfg.port); + let url = format!("{}:{}", config.interface, config.port); let addr = url.parse().map_err(|_| format!("Invalid JSONRPC listen host/port given: {}", url))?; - let server = setup_http_rpc_server(&addr, cfg.cors.clone(), cfg.hosts.clone(), enable_devel_api, deps)?; - cinfo!(RPC, "RPC Listening on {}", url); - if let Some(hosts) = cfg.hosts { - cinfo!(RPC, "Allowed hosts are {:?}", hosts); - } - if let Some(cors) = cfg.cors { - cinfo!(RPC, "CORS domains are {:?}", cors); - } - Ok(server) -} - -fn setup_http_rpc_server( - url: &SocketAddr, - cors_domains: Option<Vec<String>>, - allowed_hosts: Option<Vec<String>>, - enable_devel_api: bool, - deps: &rpc_apis::ApiDependencies, -) -> Result<HttpServer, String> { - let server = setup_rpc_server(enable_devel_api, deps); - let start_result = start_http(url, cors_domains, allowed_hosts, server); + let start_result = start_http(&addr, config.cors.clone(), config.hosts.clone(), server); match start_result { Err(ref err) if err.kind() == io::ErrorKind::AddrInUse => { Err(format!("RPC address {} is already in use, make sure that another instance of a CodeChain node is not running or change the address using the --jsonrpc-port option.", url)) }, Err(e) => Err(format!("RPC error: {:?}", e)), - Ok(server) => Ok(server), + Ok(server) => { + cinfo!(RPC, "RPC Listening on {}", url); + if let Some(hosts) = config.hosts { + cinfo!(RPC, "Allowed hosts are {:?}", hosts); + } + if let Some(cors) = config.cors { + cinfo!(RPC, "CORS domains are {:?}", cors); + } + Ok(server) + }, } } @@ -74,19 +63,17 @@ pub struct RpcIpcConfig { } pub fn rpc_ipc_start( - cfg: &RpcIpcConfig, - enable_devel_api: bool, - deps: &rpc_apis::ApiDependencies, + server: MetaIoHandler<(), impl Middleware<()>>, + config: RpcIpcConfig, ) -> Result<IpcServer, String> { - let server = setup_rpc_server(enable_devel_api, deps); - let start_result = start_ipc(&cfg.socket_addr, server); + let start_result = start_ipc(&config.socket_addr, server); match start_result { Err(ref err) if err.kind() == io::ErrorKind::AddrInUse => { - Err(format!("IPC address {} is already in use, make sure that another instance of a Codechain node is not running or change the address using the --ipc-path options.", cfg.socket_addr)) + Err(format!("IPC address {} is already in use, make sure that another instance of a Codechain node is not running or change the address using the --ipc-path options.", config.socket_addr)) }, Err(e) => Err(format!("IPC error: {:?}", e)), Ok(server) => { - cinfo!(RPC, "IPC Listening on {}", cfg.socket_addr); + cinfo!(RPC, "IPC Listening on {}", config.socket_addr); Ok(server) }, } @@ -99,15 +86,10 @@ pub struct RpcWsConfig { pub max_connections: usize, } -pub fn rpc_ws_start( - cfg: &RpcWsConfig, - enable_devel_api: bool, - deps: &rpc_apis::ApiDependencies, -) -> Result<WsServer, String> { - let server = setup_rpc_server(enable_devel_api, deps); - let url = format!("{}:{}", cfg.interface, cfg.port); +pub fn rpc_ws_start(server: MetaIoHandler<(), impl Middleware<()>>, config: RpcWsConfig) -> Result<WsServer, String> { + let url = format!("{}:{}", config.interface, config.port); let addr = url.parse().map_err(|_| format!("Invalid WebSockets listen host/port given: {}", url))?; - let start_result = start_ws(&addr, server, cfg.max_connections); + let start_result = start_ws(&addr, server, config.max_connections); match start_result { Err(WsError::Io(ref err)) if err.kind() == io::ErrorKind::AddrInUse => { Err(format!("WebSockets address {} is already in use, make sure that another instance of a Codechain node is not running or change the address using the --ws-port options.", addr)) @@ -120,12 +102,9 @@ pub fn rpc_ws_start( } } -fn setup_rpc_server( - enable_devel_api: bool, - deps: &rpc_apis::ApiDependencies, -) -> MetaIoHandler<(), impl Middleware<()>> { +pub fn setup_rpc_server(config: &Config, deps: &rpc_apis::ApiDependencies) -> MetaIoHandler<(), impl Middleware<()>> { let mut handler = MetaIoHandler::with_middleware(LogMiddleware::new()); - deps.extend_api(enable_devel_api, &mut handler); + deps.extend_api(config, &mut handler); rpc_apis::setup_rpc(handler) } diff --git a/codechain/rpc_apis.rs b/codechain/rpc_apis.rs index 3f4d3c016a..bc701275df 100644 --- a/codechain/rpc_apis.rs +++ b/codechain/rpc_apis.rs @@ -22,6 +22,8 @@ use cnetwork::{EventSender, NetworkControl}; use crpc::{MetaIoHandler, Middleware, Params, Value}; use csync::BlockSyncEvent; +use crate::config::Config; + pub struct ApiDependencies { pub client: Arc<Client>, pub miner: Arc<Miner>, @@ -31,11 +33,12 @@ pub struct ApiDependencies { } impl ApiDependencies { - pub fn extend_api(&self, enable_devel_api: bool, handler: &mut MetaIoHandler<(), impl Middleware<()>>) { + pub fn extend_api(&self, config: &Config, handler: &mut MetaIoHandler<(), impl Middleware<()>>) { use crpc::v1::*; handler.extend_with(ChainClient::new(Arc::clone(&self.client)).to_delegate()); handler.extend_with(MempoolClient::new(Arc::clone(&self.client)).to_delegate()); - if enable_devel_api { + handler.extend_with(SnapshotClient::new(Arc::clone(&self.client), config.snapshot.path.clone()).to_delegate()); + if config.rpc.enable_devel_api { handler.extend_with( DevelClient::new(Arc::clone(&self.client), Arc::clone(&self.miner), self.block_sync.clone()) .to_delegate(), diff --git a/codechain/run_node.rs b/codechain/run_node.rs index 9ec3646293..bf04996b99 100644 --- a/codechain/run_node.rs +++ b/codechain/run_node.rs @@ -19,6 +19,7 @@ use std::path::Path; use std::sync::{Arc, Weak}; use std::time::{SystemTime, UNIX_EPOCH}; +use ccore::snapshot_notify; use ccore::{ AccountProvider, AccountProviderError, BlockId, ChainNotify, Client, ClientConfig, ClientService, EngineInfo, EngineType, Miner, MinerService, Scheme, Stratum, StratumConfig, StratumError, NUM_COLUMNS, @@ -30,9 +31,11 @@ use ckeystore::KeyStore; use clap::ArgMatches; use clogger::{self, EmailAlarm, LoggerConfig}; use cnetwork::{Filters, NetworkConfig, NetworkControl, NetworkService, RoutingTable, SocketAddr}; -use csync::{BlockSyncExtension, BlockSyncSender, SnapshotService, TransactionSyncExtension}; +use csync::snapshot::Service as SnapshotService; +use csync::{BlockSyncExtension, BlockSyncSender, TransactionSyncExtension}; use ctimer::TimerLoop; use ctrlc::CtrlC; +use ctypes::BlockHash; use fdlimit::raise_fd_limit; use kvdb::KeyValueDB; use kvdb_rocksdb::{Database, DatabaseConfig}; @@ -42,7 +45,7 @@ use crate::config::{self, load_config}; use crate::constants::{DEFAULT_DB_PATH, DEFAULT_KEYS_PATH}; use crate::dummy_network_service::DummyNetworkService; use crate::json::PasswordFile; -use crate::rpc::{rpc_http_start, rpc_ipc_start, rpc_ws_start}; +use crate::rpc::{rpc_http_start, rpc_ipc_start, rpc_ws_start, setup_rpc_server}; use crate::rpc_apis::ApiDependencies; fn network_start( @@ -283,7 +286,7 @@ pub fn run_node(matches: &ArgMatches) -> Result<(), String> { let network_config = config.network_config()?; // XXX: What should we do if the network id has been changed. let c = client.client(); - let network_id = c.common_params(BlockId::Latest).unwrap().network_id(); + let network_id = c.common_params(BlockId::Number(0)).unwrap().network_id(); let routing_table = RoutingTable::new(); let service = network_start(network_id, timer_loop, &network_config, Arc::clone(&routing_table))?; @@ -296,7 +299,14 @@ pub fn run_node(matches: &ArgMatches) -> Result<(), String> { if config.network.sync.unwrap() { let sync_sender = { let client = client.client(); - service.register_extension(move |api| BlockSyncExtension::new(client, api)) + let snapshot_target = match (config.network.snapshot_hash, config.network.snapshot_number) { + (Some(hash), Some(num)) => Some((BlockHash::from(hash), num)), + _ => None, + }; + let snapshot_dir = config.snapshot.path.clone(); + service.register_extension(move |api| { + BlockSyncExtension::new(client, api, snapshot_target, snapshot_dir) + }) }; let sync = Arc::new(BlockSyncSender::from(sync_sender.clone())); client.client().add_notify(Arc::downgrade(&sync) as Weak<dyn ChainNotify>); @@ -316,36 +326,43 @@ pub fn run_node(matches: &ArgMatches) -> Result<(), String> { } }; - let rpc_apis_deps = ApiDependencies { - client: client.client(), - miner: Arc::clone(&miner), - network_control: Arc::clone(&network_service), - account_provider: ap, - block_sync: maybe_sync_sender, - }; + let (rpc_server, ipc_server, ws_server) = { + let rpc_apis_deps = ApiDependencies { + client: client.client(), + miner: Arc::clone(&miner), + network_control: Arc::clone(&network_service), + account_provider: ap, + block_sync: maybe_sync_sender, + }; + + let rpc_server = { + if !config.rpc.disable.unwrap() { + let server = setup_rpc_server(&config, &rpc_apis_deps); + Some(rpc_http_start(server, config.rpc_http_config())?) + } else { + None + } + }; - let rpc_server = { - if !config.rpc.disable.unwrap() { - Some(rpc_http_start(config.rpc_http_config(), config.rpc.enable_devel_api, &rpc_apis_deps)?) - } else { - None - } - }; + let ipc_server = { + if !config.ipc.disable.unwrap() { + let server = setup_rpc_server(&config, &rpc_apis_deps); + Some(rpc_ipc_start(server, config.rpc_ipc_config())?) + } else { + None + } + }; - let ipc_server = { - if !config.ipc.disable.unwrap() { - Some(rpc_ipc_start(&config.rpc_ipc_config(), config.rpc.enable_devel_api, &rpc_apis_deps)?) - } else { - None - } - }; + let ws_server = { + if !config.ws.disable.unwrap() { + let server = setup_rpc_server(&config, &rpc_apis_deps); + Some(rpc_ws_start(server, config.rpc_ws_config())?) + } else { + None + } + }; - let ws_server = { - if !config.ws.disable.unwrap() { - Some(rpc_ws_start(&config.rpc_ws_config(), config.rpc.enable_devel_api, &rpc_apis_deps)?) - } else { - None - } + (rpc_server, ipc_server, ws_server) }; if (!config.stratum.disable.unwrap()) && (miner.engine_type() == EngineType::PoW) { @@ -353,12 +370,13 @@ pub fn run_node(matches: &ArgMatches) -> Result<(), String> { } let _snapshot_service = { + let client = client.client(); + let (tx, rx) = snapshot_notify::create(); + client.engine().register_snapshot_notify_sender(tx); + if !config.snapshot.disable.unwrap() { - // FIXME: Let's make it load snapshot period dynamically to support changing the period. - let client = client.client(); - let snapshot_period = client.common_params(BlockId::Latest).unwrap().snapshot_period(); - let service = SnapshotService::new(Arc::clone(&client), config.snapshot.path.unwrap(), snapshot_period); - client.add_notify(Arc::downgrade(&service) as Weak<dyn ChainNotify>); + let service = + Arc::new(SnapshotService::new(client, rx, config.snapshot.path.unwrap(), config.snapshot.expiration)); Some(service) } else { None @@ -367,6 +385,7 @@ pub fn run_node(matches: &ArgMatches) -> Result<(), String> { // drop the scheme to free up genesis state. drop(scheme); + client.client().engine().register_is_done(); cinfo!(TEST_SCRIPT, "Initialization complete"); diff --git a/core/src/block.rs b/core/src/block.rs index 103d5903a7..ab35efe3cf 100644 --- a/core/src/block.rs +++ b/core/src/block.rs @@ -33,6 +33,7 @@ use crate::client::{EngineInfo, TermInfo}; use crate::consensus::CodeChainEngine; use crate::error::{BlockError, Error}; use crate::transaction::{SignedTransaction, UnverifiedTransaction}; +use crate::BlockId; /// A block, encoded as it is on the block chain. #[derive(Debug, Clone, PartialEq)] @@ -220,25 +221,20 @@ impl<'x> OpenBlock<'x> { pub fn close( mut self, parent_header: &Header, - parent_common_params: &CommonParams, term_common_params: Option<&CommonParams>, ) -> Result<ClosedBlock, Error> { let unclosed_state = self.block.state.clone(); - if let Err(e) = - self.engine.on_close_block(&mut self.block, parent_header, parent_common_params, term_common_params) - { + if let Err(e) = self.engine.on_close_block(&mut self.block, term_common_params) { warn!("Encountered error on closing the block: {}", e); return Err(e) } let header = self.block.header().clone(); for handler in self.engine.action_handlers() { - handler.on_close_block(self.block.state_mut(), &header, parent_header, parent_common_params).map_err( - |e| { - warn!("Encountered error in {}::on_close_block", handler.name()); - e - }, - )?; + handler.on_close_block(self.block.state_mut(), &header).map_err(|e| { + warn!("Encountered error in {}::on_close_block", handler.name()); + e + })?; } let state_root = self.block.state.commit().map_err(|e| { @@ -262,23 +258,18 @@ impl<'x> OpenBlock<'x> { pub fn close_and_lock( mut self, parent_header: &Header, - parent_common_params: &CommonParams, term_common_params: Option<&CommonParams>, ) -> Result<LockedBlock, Error> { - if let Err(e) = - self.engine.on_close_block(&mut self.block, parent_header, parent_common_params, term_common_params) - { + if let Err(e) = self.engine.on_close_block(&mut self.block, term_common_params) { warn!("Encountered error on closing the block: {}", e); return Err(e) } let header = self.block.header().clone(); for handler in self.engine.action_handlers() { - handler.on_close_block(self.block.state_mut(), &header, parent_header, parent_common_params).map_err( - |e| { - warn!("Encountered error in {}::on_close_block", handler.name()); - e - }, - )?; + handler.on_close_block(self.block.state_mut(), &header).map_err(|e| { + warn!("Encountered error in {}::on_close_block", handler.name()); + e + })?; } let state_root = self.block.state.commit().map_err(|e| { @@ -322,6 +313,10 @@ impl<'x> OpenBlock<'x> { self.block.header.set_seal(seal); Ok(()) } + + pub fn inner_mut(&mut self) -> &mut ExecutedBlock { + &mut self.block + } } /// Just like `OpenBlock`, except that we've applied `Engine::on_close_block`, finished up the non-seal header fields. @@ -501,20 +496,11 @@ pub fn enact<C: ChainTimeInfo + EngineInfo + FindActionHandler + TermInfo>( let mut b = OpenBlock::try_new(engine, db, parent, Address::default(), vec![])?; b.populate_from(header); + engine.on_open_block(b.inner_mut())?; b.push_transactions(transactions, client, parent.number(), parent.timestamp())?; - let parent_common_params = client.common_params((*header.parent_hash()).into()).unwrap(); - let term_common_params = { - let block_number = client - .last_term_finished_block_num((*header.parent_hash()).into()) - .expect("The block of the parent hash should exist"); - if block_number == 0 { - None - } else { - Some(client.common_params((block_number).into()).expect("Common params should exist")) - } - }; - b.close_and_lock(parent, &parent_common_params, term_common_params.as_ref()) + let term_common_params = client.term_common_params(BlockId::Hash(*header.parent_hash())); + b.close_and_lock(parent, term_common_params.as_ref()) } #[cfg(test)] @@ -532,9 +518,8 @@ mod tests { let genesis_header = scheme.genesis_header(); let db = scheme.ensure_genesis_state(get_temp_state_db()).unwrap(); let b = OpenBlock::try_new(&*scheme.engine, db, &genesis_header, Address::default(), vec![]).unwrap(); - let parent_common_params = CommonParams::default_for_test(); let term_common_params = CommonParams::default_for_test(); - let b = b.close_and_lock(&genesis_header, &parent_common_params, Some(&term_common_params)).unwrap(); + let b = b.close_and_lock(&genesis_header, Some(&term_common_params)).unwrap(); let _ = b.seal(&*scheme.engine, vec![]); } } diff --git a/core/src/blockchain/blockchain.rs b/core/src/blockchain/blockchain.rs index 0ed70027f2..bbf028afe3 100644 --- a/core/src/blockchain/blockchain.rs +++ b/core/src/blockchain/blockchain.rs @@ -110,6 +110,45 @@ impl BlockChain { } } + pub fn insert_floating_header(&self, batch: &mut DBTransaction, header: &HeaderView) { + self.headerchain.insert_floating_header(batch, header); + } + + pub fn insert_floating_block(&self, batch: &mut DBTransaction, bytes: &[u8]) { + let block = BlockView::new(bytes); + let header = block.header_view(); + let hash = header.hash(); + + ctrace!(BLOCKCHAIN, "Inserting bootstrap block #{}({}) to the blockchain.", header.number(), hash); + + if self.is_known(&hash) { + cdebug!(BLOCKCHAIN, "Block #{}({}) is already known.", header.number(), hash); + return + } + + self.insert_floating_header(batch, &header); + self.body_db.insert_body(batch, &block); + } + + pub fn force_update_best_block(&self, batch: &mut DBTransaction, hash: &BlockHash) { + ctrace!(BLOCKCHAIN, "Forcefully updating the best block to {}", hash); + + assert!(self.is_known(hash)); + assert!(self.pending_best_block_hash.read().is_none()); + assert!(self.pending_best_proposal_block_hash.read().is_none()); + + let block = self.block(hash).expect("Target block is known"); + self.headerchain.force_update_best_header(batch, hash); + self.body_db.update_best_block(batch, &BestBlockChanged::CanonChainAppended { + best_block: block.into_inner(), + }); + + batch.put(db::COL_EXTRA, BEST_BLOCK_KEY, hash); + *self.pending_best_block_hash.write() = Some(*hash); + batch.put(db::COL_EXTRA, BEST_PROPOSAL_BLOCK_KEY, hash); + *self.pending_best_proposal_block_hash.write() = Some(*hash); + } + /// Inserts the block into backing cache database. /// Expects the block to be valid and already verified. /// If the block is already known, does nothing. diff --git a/core/src/blockchain/headerchain.rs b/core/src/blockchain/headerchain.rs index 10700264a5..3aa3d3ec74 100644 --- a/core/src/blockchain/headerchain.rs +++ b/core/src/blockchain/headerchain.rs @@ -115,6 +115,50 @@ impl HeaderChain { } } + /// Inserts a floating header into backing cache database. + /// Expects the header to be valid. + /// If the header is already known, does nothing. + pub fn insert_floating_header(&self, batch: &mut DBTransaction, header: &HeaderView) { + let hash = header.hash(); + + ctrace!(HEADERCHAIN, "Inserting a floating block header #{}({}) to the headerchain.", header.number(), hash); + + if self.is_known_header(&hash) { + ctrace!(HEADERCHAIN, "Block header #{}({}) is already known.", header.number(), hash); + return + } + + let compressed_header = compress(header.rlp().as_raw(), blocks_swapper()); + batch.put(db::COL_HEADERS, &hash, &compressed_header); + + let mut new_hashes = HashMap::new(); + new_hashes.insert(header.number(), hash); + let mut new_details = HashMap::new(); + new_details.insert(hash, BlockDetails { + number: header.number(), + total_score: 0.into(), + parent: header.parent_hash(), + }); + + let mut pending_hashes = self.pending_hashes.write(); + let mut pending_details = self.pending_details.write(); + + batch.extend_with_cache(db::COL_EXTRA, &mut *pending_details, new_details, CacheUpdatePolicy::Overwrite); + batch.extend_with_cache(db::COL_EXTRA, &mut *pending_hashes, new_hashes, CacheUpdatePolicy::Overwrite); + } + + pub fn force_update_best_header(&self, batch: &mut DBTransaction, hash: &BlockHash) { + ctrace!(HEADERCHAIN, "Forcefully updating the best header to {}", hash); + assert!(self.is_known_header(hash)); + assert!(self.pending_best_header_hash.read().is_none()); + assert!(self.pending_best_proposal_block_hash.read().is_none()); + + batch.put(db::COL_EXTRA, BEST_HEADER_KEY, hash); + *self.pending_best_header_hash.write() = Some(*hash); + batch.put(db::COL_EXTRA, BEST_PROPOSAL_HEADER_KEY, hash); + *self.pending_best_proposal_block_hash.write() = Some(*hash); + } + /// Inserts the header into backing cache database. /// Expects the header to be valid and already verified. /// If the header is already known, does nothing. diff --git a/core/src/client/client.rs b/core/src/client/client.rs index d0617431c1..22c6192684 100644 --- a/core/src/client/client.rs +++ b/core/src/client/client.rs @@ -26,6 +26,7 @@ use cstate::{ ActionHandler, AssetScheme, FindActionHandler, OwnedAsset, StateDB, StateResult, Text, TopLevelState, TopStateView, }; use ctimer::{TimeoutHandler, TimerApi, TimerScheduleError, TimerToken}; +use ctypes::header::Header; use ctypes::transaction::{AssetTransferInput, PartialHashing, ShardTransaction}; use ctypes::{BlockHash, BlockNumber, CommonParams, ShardId, Tracker, TxHash}; use cvm::{decode, execute, ChainTimeInfo, ScriptResult, VMConfig}; @@ -42,9 +43,9 @@ use super::{ ClientConfig, DatabaseClient, EngineClient, EngineInfo, Error as ClientError, ExecuteClient, ImportBlock, ImportResult, MiningBlockChainClient, Shard, StateInfo, StateOrBlock, TextClient, }; -use crate::block::{ClosedBlock, IsBlock, OpenBlock, SealedBlock}; +use crate::block::{Block, ClosedBlock, IsBlock, OpenBlock, SealedBlock}; use crate::blockchain::{BlockChain, BlockProvider, BodyProvider, HeaderProvider, InvoiceProvider, TransactionAddress}; -use crate::client::{ConsensusClient, TermInfo}; +use crate::client::{ConsensusClient, SnapshotClient, TermInfo}; use crate::consensus::{CodeChainEngine, EngineError}; use crate::encoded; use crate::error::{BlockImportError, Error, ImportError, SchemeError}; @@ -655,6 +656,28 @@ impl ImportBlock for Client { Ok(self.importer.header_queue.import(unverified)?) } + fn import_trusted_header(&self, header: &Header) -> Result<BlockHash, BlockImportError> { + if self.block_chain().is_known_header(&header.hash()) { + return Err(BlockImportError::Import(ImportError::AlreadyInChain)) + } + let import_lock = self.importer.import_lock.lock(); + self.importer.import_trusted_header(header, self, &import_lock); + Ok(header.hash()) + } + + fn import_trusted_block(&self, block: &Block) -> Result<BlockHash, BlockImportError> { + if self.block_chain().is_known(&block.header.hash()) { + return Err(BlockImportError::Import(ImportError::AlreadyInChain)) + } + let import_lock = self.importer.import_lock.lock(); + self.importer.import_trusted_block(block, self, &import_lock); + Ok(block.header.hash()) + } + + fn force_update_best_block(&self, hash: &BlockHash) { + self.importer.force_update_best_block(hash, self) + } + fn import_sealed_block(&self, block: &SealedBlock) -> ImportResult { let h = block.header().hash(); let route = { @@ -813,6 +836,23 @@ impl TermInfo for Client { .map(|state| state.metadata().unwrap().expect("Metadata always exist")) .map(|metadata| metadata.current_term_id()) } + + fn term_common_params(&self, id: BlockId) -> Option<CommonParams> { + let state = self.state_at(id)?; + let metadata = state.metadata().unwrap().expect("Metadata always exist"); + + if let Some(term_params) = metadata.term_params() { + Some(*term_params) + } else { + let block_number = + self.last_term_finished_block_num(id).expect("The block of the parent hash should exist"); + if block_number == 0 { + None + } else { + Some(self.common_params((block_number).into()).expect("Common params should exist")) + } + } + } } impl AccountData for Client { @@ -926,3 +966,11 @@ impl FindActionHandler for Client { self.engine.find_action_handler_for(id) } } + +impl SnapshotClient for Client { + fn notify_snapshot(&self, id: BlockId) { + if let Some(header) = self.block_header(&id) { + self.engine.send_snapshot_notify(header.hash()) + } + } +} diff --git a/core/src/client/importer.rs b/core/src/client/importer.rs index 8400fc7c36..4290ee7ab9 100644 --- a/core/src/client/importer.rs +++ b/core/src/client/importer.rs @@ -19,14 +19,14 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use cio::IoChannel; -use ctypes::header::Header; +use ctypes::header::{Header, Seal}; use ctypes::BlockHash; use kvdb::DBTransaction; use parking_lot::{Mutex, MutexGuard}; use rlp::Encodable; use super::{BlockChainTrait, Client, ClientConfig}; -use crate::block::{enact, IsBlock, LockedBlock}; +use crate::block::{enact, Block, IsBlock, LockedBlock}; use crate::blockchain::{BodyProvider, HeaderProvider, ImportRoute}; use crate::client::EngineInfo; use crate::consensus::CodeChainEngine; @@ -100,7 +100,7 @@ impl Importer { } { - let headers: Vec<&Header> = blocks.iter().map(|block| &block.header).collect(); + let headers: Vec<_> = blocks.iter().map(|block| &block.header).collect(); self.import_headers(headers, client, &import_lock); } @@ -362,6 +362,51 @@ impl Importer { imported.len() } + pub fn import_trusted_header<'a>(&'a self, header: &'a Header, client: &Client, _importer_lock: &MutexGuard<()>) { + let hash = header.hash(); + ctrace!(CLIENT, "Importing trusted header #{}-{:?}", header.number(), hash); + + { + let chain = client.block_chain(); + let mut batch = DBTransaction::new(); + chain.insert_floating_header(&mut batch, &HeaderView::new(&header.rlp_bytes())); + client.db().write_buffered(batch); + chain.commit(); + } + client.new_headers(&[hash], &[], &[], &[], &[], None); + + client.db().flush().expect("DB flush failed."); + } + + pub fn import_trusted_block<'a>(&'a self, block: &'a Block, client: &Client, importer_lock: &MutexGuard<()>) { + let header = &block.header; + let hash = header.hash(); + ctrace!(CLIENT, "Importing trusted block #{}-{:?}", header.number(), hash); + + self.import_trusted_header(header, client, importer_lock); + { + let chain = client.block_chain(); + let mut batch = DBTransaction::new(); + chain.insert_floating_block(&mut batch, &block.rlp_bytes(&Seal::With)); + client.db().write_buffered(batch); + chain.commit(); + } + self.miner.chain_new_blocks(client, &[hash], &[], &[], &[]); + client.new_blocks(&[hash], &[], &[], &[], &[]); + + client.db().flush().expect("DB flush failed."); + } + + pub fn force_update_best_block(&self, hash: &BlockHash, client: &Client) { + let chain = client.block_chain(); + let mut batch = DBTransaction::new(); + chain.force_update_best_block(&mut batch, hash); + client.db().write_buffered(batch); + chain.commit(); + + client.db().flush().expect("DB flush failed."); + } + fn check_header(&self, header: &Header, parent: &Header) -> bool { // FIXME: self.verifier.verify_block_family if let Err(e) = self.engine.verify_block_family(&header, &parent) { diff --git a/core/src/client/mod.rs b/core/src/client/mod.rs index eb3adaf3b3..02c71223f4 100644 --- a/core/src/client/mod.rs +++ b/core/src/client/mod.rs @@ -20,6 +20,7 @@ mod client; mod config; mod error; mod importer; +pub mod snapshot_notify; mod test_client; pub use self::chain_notify::ChainNotify; @@ -36,13 +37,14 @@ use ckey::{Address, NetworkId, PlatformAddress, Public}; use cmerkle::Result as TrieResult; use cnetwork::NodeId; use cstate::{AssetScheme, FindActionHandler, OwnedAsset, StateResult, Text, TopLevelState, TopStateView}; +use ctypes::header::Header; use ctypes::transaction::{AssetTransferInput, PartialHashing, ShardTransaction}; use ctypes::{BlockHash, BlockNumber, CommonParams, ShardId, Tracker, TxHash}; use cvm::ChainTimeInfo; use kvdb::KeyValueDB; use primitives::{Bytes, H160, H256, U256}; -use crate::block::{ClosedBlock, OpenBlock, SealedBlock}; +use crate::block::{Block, ClosedBlock, OpenBlock, SealedBlock}; use crate::blockchain_info::BlockChainInfo; use crate::consensus::EngineError; use crate::encoded; @@ -119,6 +121,7 @@ pub trait ConsensusClient: BlockChainClient + EngineClient + EngineInfo + TermIn pub trait TermInfo { fn last_term_finished_block_num(&self, id: BlockId) -> Option<BlockNumber>; fn current_term_id(&self, id: BlockId) -> Option<u64>; + fn term_common_params(&self, id: BlockId) -> Option<CommonParams>; } /// Provides methods to access account info @@ -200,6 +203,17 @@ pub trait ImportBlock { /// Import a header into the blockchain fn import_header(&self, bytes: Bytes) -> Result<BlockHash, BlockImportError>; + /// Import a trusted header into the blockchain + /// Trusted header doesn't go through any verifications and doesn't update the best header + fn import_trusted_header(&self, header: &Header) -> Result<BlockHash, BlockImportError>; + + /// Import a trusted block into the blockchain + /// Trusted block doesn't go through any verifications and doesn't update the best block + fn import_trusted_block(&self, block: &Block) -> Result<BlockHash, BlockImportError>; + + /// Forcefully update the best block + fn force_update_best_block(&self, hash: &BlockHash); + /// Import sealed block. Skips all verifications. fn import_sealed_block(&self, block: &SealedBlock) -> ImportResult; @@ -342,3 +356,7 @@ pub trait StateInfo { /// is unknown. fn state_at(&self, id: BlockId) -> Option<TopLevelState>; } + +pub trait SnapshotClient { + fn notify_snapshot(&self, id: BlockId); +} diff --git a/core/src/client/snapshot_notify.rs b/core/src/client/snapshot_notify.rs new file mode 100644 index 0000000000..8e0a372cbb --- /dev/null +++ b/core/src/client/snapshot_notify.rs @@ -0,0 +1,81 @@ +use ctypes::BlockHash; + +use parking_lot::RwLock; +use std::sync::mpsc::{sync_channel, Receiver, RecvError, SyncSender}; +use std::sync::{Arc, Weak}; + +pub fn create() -> (NotifySender, NotifyReceiverSource) { + let (tx, rx) = sync_channel(1); + let tx = Arc::new(RwLock::new(Some(tx))); + let tx_weak = Arc::downgrade(&tx); + ( + NotifySender { + tx, + }, + NotifyReceiverSource( + ReceiverCanceller { + tx: tx_weak, + }, + NotifyReceiver { + rx, + }, + ), + ) +} + +pub struct NotifySender { + tx: Arc<RwLock<Option<SyncSender<BlockHash>>>>, +} + +impl NotifySender { + pub fn notify(&self, block_hash: BlockHash) { + let guard = self.tx.read(); + if let Some(tx) = guard.as_ref() { + // TODO: Ignore the error. Receiver thread might be terminated or congested. + let _ = tx.try_send(block_hash); + } else { + // ReceiverCanceller is dropped. + } + } +} + +pub struct NotifyReceiverSource(pub ReceiverCanceller, pub NotifyReceiver); + +/// Dropping this makes the receiver stopped. +/// +/// `recv()` method of the `Receiver` will stop and return `RecvError` when corresponding `Sender` is dropped. +/// This is an inherited behaviour of `std::sync::mpsc::{Sender, Receiver}`. +/// However, we need another way to stop the `Receiver`, since `Sender` is usually shared throughout our codes. +/// We can't collect them all and destory one by one. We need a kill switch. +/// +/// `ReceiverCanceller` holds weak reference to the `Sender`, so it doesn't prohibit the default behaviour. +/// Then, we can upgrade the weak reference and get the shared reference to `Sender` itself, and manually drop it with this. +pub struct ReceiverCanceller { + tx: Weak<RwLock<Option<SyncSender<BlockHash>>>>, +} + +impl Drop for ReceiverCanceller { + fn drop(&mut self) { + if let Some(tx) = self.tx.upgrade() { + let mut guard = tx.write(); + if let Some(sender) = guard.take() { + drop(sender) + } + } else { + // All NotifySender is dropped. No droppable Sender. + } + } +} + +/// Receiver is dropped when +/// 1. There are no NotifySenders out there. +/// 2. ReceiverCanceller is dropped. See the comment of `ReceiverCanceller`. +pub struct NotifyReceiver { + rx: Receiver<BlockHash>, +} + +impl NotifyReceiver { + pub fn recv(&self) -> Result<BlockHash, RecvError> { + self.rx.recv() + } +} diff --git a/core/src/client/test_client.rs b/core/src/client/test_client.rs index 0a279626e7..b2b0514778 100644 --- a/core/src/client/test_client.rs +++ b/core/src/client/test_client.rs @@ -42,6 +42,7 @@ use cnetwork::NodeId; use cstate::tests::helpers::empty_top_state; use cstate::{FindActionHandler, StateDB, TopLevelState}; use ctimer::{TimeoutHandler, TimerToken}; +use ctypes::header::Header; use ctypes::transaction::{Action, Transaction}; use ctypes::{BlockHash, BlockNumber, CommonParams, Header as BlockHeader, Tracker, TxHash}; use cvm::ChainTimeInfo; @@ -52,7 +53,7 @@ use parking_lot::RwLock; use primitives::{Bytes, H256, U256}; use rlp::*; -use crate::block::{ClosedBlock, OpenBlock, SealedBlock}; +use crate::block::{Block, ClosedBlock, OpenBlock, SealedBlock}; use crate::blockchain_info::BlockChainInfo; use crate::client::{ AccountData, BlockChainClient, BlockChainTrait, BlockProducer, BlockStatus, ConsensusClient, EngineInfo, @@ -509,6 +510,18 @@ impl ImportBlock for TestBlockChainClient { unimplemented!() } + fn import_trusted_header(&self, _header: &Header) -> Result<BlockHash, BlockImportError> { + unimplemented!() + } + + fn import_trusted_block(&self, _block: &Block) -> Result<BlockHash, BlockImportError> { + unimplemented!() + } + + fn force_update_best_block(&self, _hash: &BlockHash) { + unimplemented!() + } + fn import_sealed_block(&self, _block: &SealedBlock) -> ImportResult { Ok(H256::default().into()) } @@ -645,7 +658,7 @@ impl super::EngineClient for TestBlockChainClient { impl EngineInfo for TestBlockChainClient { fn common_params(&self, _block_id: BlockId) -> Option<CommonParams> { - unimplemented!() + Some(*self.scheme.engine.machine().genesis_common_params()) } fn metadata_seq(&self, _block_id: BlockId) -> Option<u64> { @@ -679,6 +692,10 @@ impl TermInfo for TestBlockChainClient { fn current_term_id(&self, _id: BlockId) -> Option<u64> { self.term_id } + + fn term_common_params(&self, _id: BlockId) -> Option<CommonParams> { + None + } } impl StateInfo for TestBlockChainClient { diff --git a/core/src/consensus/blake_pow/mod.rs b/core/src/consensus/blake_pow/mod.rs index 8e0b20a6e3..010fa35ae6 100644 --- a/core/src/consensus/blake_pow/mod.rs +++ b/core/src/consensus/blake_pow/mod.rs @@ -163,8 +163,6 @@ impl ConsensusEngine for BlakePoW { fn on_close_block( &self, block: &mut ExecutedBlock, - _parent_header: &Header, - _parent_common_params: &CommonParams, _term_common_params: Option<&CommonParams>, ) -> Result<(), Error> { let author = *block.header().author(); diff --git a/core/src/consensus/cuckoo/mod.rs b/core/src/consensus/cuckoo/mod.rs index ad088f2454..d904120d67 100644 --- a/core/src/consensus/cuckoo/mod.rs +++ b/core/src/consensus/cuckoo/mod.rs @@ -173,8 +173,6 @@ impl ConsensusEngine for Cuckoo { fn on_close_block( &self, block: &mut ExecutedBlock, - _parent_header: &Header, - _parent_common_params: &CommonParams, _term_common_params: Option<&CommonParams>, ) -> Result<(), Error> { let author = *block.header().author(); @@ -261,21 +259,13 @@ mod tests { #[test] fn on_close_block() { let scheme = Scheme::new_test_cuckoo(); - let genesis_header = scheme.genesis_header(); let engine = &*scheme.engine; let db = scheme.ensure_genesis_state(get_temp_state_db()).unwrap(); let header = Header::default(); let block = OpenBlock::try_new(engine, db, &header, Default::default(), vec![]).unwrap(); let mut executed_block = block.block().clone(); - assert!(engine - .on_close_block( - &mut executed_block, - &genesis_header, - &CommonParams::default_for_test(), - Some(&CommonParams::default_for_test()) - ) - .is_ok()); + assert!(engine.on_close_block(&mut executed_block, Some(&CommonParams::default_for_test())).is_ok()); assert_eq!(0xd, engine.machine().balance(&executed_block, header.author()).unwrap()); } diff --git a/core/src/consensus/mod.rs b/core/src/consensus/mod.rs index 6841b859d4..843f2f2663 100644 --- a/core/src/consensus/mod.rs +++ b/core/src/consensus/mod.rs @@ -51,6 +51,7 @@ use primitives::{Bytes, U256}; use self::bit_set::BitSet; use crate::account_provider::AccountProvider; use crate::block::{ExecutedBlock, SealedBlock}; +use crate::client::snapshot_notify::NotifySender as SnapshotNotifySender; use crate::client::ConsensusClient; use crate::codechain_machine::CodeChainMachine; use crate::error::Error; @@ -221,12 +222,15 @@ pub trait ConsensusEngine: Sync + Send { /// Stops any services that the may hold the Engine and makes it safe to drop. fn stop(&self) {} + /// Block transformation functions, before the transactions. + fn on_open_block(&self, _block: &mut ExecutedBlock) -> Result<(), Error> { + Ok(()) + } + /// Block transformation functions, after the transactions. fn on_close_block( &self, _block: &mut ExecutedBlock, - _parent_header: &Header, - _parent_common_params: &CommonParams, _term_common_params: Option<&CommonParams>, ) -> Result<(), Error> { Ok(()) @@ -262,6 +266,12 @@ pub trait ConsensusEngine: Sync + Send { fn register_chain_notify(&self, _: &Client) {} + fn register_snapshot_notify_sender(&self, _sender: SnapshotNotifySender) {} + + fn register_is_done(&self) {} + + fn send_snapshot_notify(&self, _block_hash: BlockHash) {} + fn get_best_block_from_best_proposal_header(&self, header: &HeaderView) -> BlockHash { header.hash() } diff --git a/core/src/consensus/null_engine/mod.rs b/core/src/consensus/null_engine/mod.rs index 832f6165b7..26f40199d1 100644 --- a/core/src/consensus/null_engine/mod.rs +++ b/core/src/consensus/null_engine/mod.rs @@ -17,7 +17,7 @@ mod params; use ckey::Address; -use ctypes::{CommonParams, Header}; +use ctypes::CommonParams; use self::params::NullEngineParams; use super::ConsensusEngine; @@ -58,8 +58,6 @@ impl ConsensusEngine for NullEngine { fn on_close_block( &self, block: &mut ExecutedBlock, - _parent_header: &Header, - _parent_common_params: &CommonParams, _term_common_params: Option<&CommonParams>, ) -> Result<(), Error> { let (author, total_reward) = { diff --git a/core/src/consensus/simple_poa/mod.rs b/core/src/consensus/simple_poa/mod.rs index 848af86b70..a6b72120b8 100644 --- a/core/src/consensus/simple_poa/mod.rs +++ b/core/src/consensus/simple_poa/mod.rs @@ -121,8 +121,6 @@ impl ConsensusEngine for SimplePoA { fn on_close_block( &self, block: &mut ExecutedBlock, - _parent_header: &Header, - _parent_common_params: &CommonParams, _term_common_params: Option<&CommonParams>, ) -> Result<(), Error> { let author = *block.header().author(); @@ -186,9 +184,8 @@ mod tests { let db = scheme.ensure_genesis_state(get_temp_state_db()).unwrap(); let genesis_header = scheme.genesis_header(); let b = OpenBlock::try_new(engine, db, &genesis_header, Default::default(), vec![]).unwrap(); - let parent_common_params = CommonParams::default_for_test(); let term_common_params = CommonParams::default_for_test(); - let b = b.close_and_lock(&genesis_header, &parent_common_params, Some(&term_common_params)).unwrap(); + let b = b.close_and_lock(&genesis_header, Some(&term_common_params)).unwrap(); if let Some(seal) = engine.generate_seal(Some(b.block()), &genesis_header).seal_fields() { assert!(b.try_seal(engine, seal).is_ok()); } diff --git a/core/src/consensus/solo/mod.rs b/core/src/consensus/solo/mod.rs index 6faf5dc0d5..0c7632e54e 100644 --- a/core/src/consensus/solo/mod.rs +++ b/core/src/consensus/solo/mod.rs @@ -16,25 +16,30 @@ mod params; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use ckey::Address; use cstate::{ActionHandler, HitHandler}; -use ctypes::{CommonParams, Header}; +use ctypes::{BlockHash, CommonParams, Header}; +use parking_lot::RwLock; use self::params::SoloParams; use super::stake; use super::{ConsensusEngine, Seal}; use crate::block::{ExecutedBlock, IsBlock}; +use crate::client::snapshot_notify::NotifySender; +use crate::client::ConsensusClient; use crate::codechain_machine::CodeChainMachine; use crate::consensus::{EngineError, EngineType}; use crate::error::Error; /// A consensus engine which does not provide any consensus mechanism. pub struct Solo { + client: RwLock<Option<Weak<dyn ConsensusClient>>>, params: SoloParams, machine: CodeChainMachine, action_handlers: Vec<Arc<dyn ActionHandler>>, + snapshot_notify_sender: Arc<RwLock<Option<NotifySender>>>, } impl Solo { @@ -47,11 +52,17 @@ impl Solo { action_handlers.push(Arc::new(stake::Stake::new(params.genesis_stakes.clone()))); Solo { + client: Default::default(), params, machine, action_handlers, + snapshot_notify_sender: Arc::new(RwLock::new(None)), } } + + fn client(&self) -> Option<Arc<dyn ConsensusClient>> { + self.client.read().as_ref()?.upgrade() + } } impl ConsensusEngine for Solo { @@ -78,10 +89,13 @@ impl ConsensusEngine for Solo { fn on_close_block( &self, block: &mut ExecutedBlock, - parent_header: &Header, - parent_common_params: &CommonParams, _term_common_params: Option<&CommonParams>, ) -> Result<(), Error> { + let client = self.client().ok_or(EngineError::CannotOpenBlock)?; + + let parent_hash = *block.header().parent_hash(); + let parent = client.block_header(&parent_hash.into()).expect("Parent header must exist"); + let parent_common_params = client.common_params(parent_hash.into()).expect("CommonParams of parent must exist"); let author = *block.header().author(); let (total_reward, total_min_fee) = { let transactions = block.transactions(); @@ -107,18 +121,18 @@ impl ConsensusEngine for Solo { self.machine.add_balance(block, &author, block_author_reward)?; return Ok(()) } - stake::add_intermediate_rewards(block.state_mut(), author, block_author_reward)?; + stake::v0::add_intermediate_rewards(block.state_mut(), author, block_author_reward)?; let last_term_finished_block_num = { let header = block.header(); let current_term_period = header.timestamp() / term_seconds; - let parent_term_period = parent_header.timestamp() / term_seconds; + let parent_term_period = parent.timestamp() / term_seconds; if current_term_period == parent_term_period { return Ok(()) } header.number() }; - stake::move_current_to_previous_intermediate_rewards(&mut block.state_mut())?; - let rewards = stake::drain_previous_rewards(&mut block.state_mut())?; + stake::v0::move_current_to_previous_intermediate_rewards(&mut block.state_mut())?; + let rewards = stake::v0::drain_previous_rewards(&mut block.state_mut())?; for (address, reward) in rewards { self.machine.add_balance(block, &address, reward)?; } @@ -127,6 +141,10 @@ impl ConsensusEngine for Solo { Ok(()) } + fn register_client(&self, client: Weak<dyn ConsensusClient>) { + *self.client.write() = Some(Weak::clone(&client)); + } + fn block_reward(&self, _block_number: u64) -> u64 { self.params.block_reward } @@ -135,6 +153,18 @@ impl ConsensusEngine for Solo { 1 } + fn register_snapshot_notify_sender(&self, sender: NotifySender) { + let mut guard = self.snapshot_notify_sender.write(); + assert!(guard.is_none(), "snapshot_notify_sender is registered twice"); + *guard = Some(sender); + } + + fn send_snapshot_notify(&self, block_hash: BlockHash) { + if let Some(sender) = self.snapshot_notify_sender.read().as_ref() { + sender.notify(block_hash) + } + } + fn action_handlers(&self) -> &[Arc<dyn ActionHandler>] { &self.action_handlers } @@ -146,25 +176,29 @@ impl ConsensusEngine for Solo { #[cfg(test)] mod tests { + use std::sync::Arc; + use ctypes::{CommonParams, Header}; use primitives::H520; use crate::block::{IsBlock, OpenBlock}; + use crate::client::{ConsensusClient, TestBlockChainClient}; use crate::scheme::Scheme; use crate::tests::helpers::get_temp_state_db; #[test] fn seal() { let scheme = Scheme::new_test_solo(); - let engine = &*scheme.engine; - let db = scheme.ensure_genesis_state(get_temp_state_db()).unwrap(); - let genesis_header = scheme.genesis_header(); - let b = OpenBlock::try_new(engine, db, &genesis_header, Default::default(), vec![]).unwrap(); - let parent_common_params = CommonParams::default_for_test(); + let client = Arc::new(TestBlockChainClient::new_with_scheme(scheme)); + let engine = client.scheme.engine.clone(); + engine.register_client(Arc::downgrade(&(client.clone() as Arc<dyn ConsensusClient>))); + let db = client.scheme.ensure_genesis_state(get_temp_state_db()).unwrap(); + let genesis_header = client.scheme.genesis_header(); + let b = OpenBlock::try_new(&*engine, db, &genesis_header, Default::default(), vec![]).unwrap(); let term_common_params = CommonParams::default_for_test(); - let b = b.close_and_lock(&genesis_header, &parent_common_params, Some(&term_common_params)).unwrap(); + let b = b.close_and_lock(&genesis_header, Some(&term_common_params)).unwrap(); if let Some(seal) = engine.generate_seal(Some(b.block()), &genesis_header).seal_fields() { - assert!(b.try_seal(engine, seal).is_ok()); + assert!(b.try_seal(&*engine, seal).is_ok()); } } diff --git a/core/src/consensus/stake/action_data.rs b/core/src/consensus/stake/action_data.rs index acc1add3b3..af14c8b33e 100644 --- a/core/src/consensus/stake/action_data.rs +++ b/core/src/consensus/stake/action_data.rs @@ -18,7 +18,6 @@ use std::cmp::Ordering; use std::collections::btree_map::{BTreeMap, Entry}; use std::collections::btree_set::{self, BTreeSet}; use std::collections::{btree_map, HashMap, HashSet}; -use std::mem; use std::ops::Deref; use std::vec; @@ -408,51 +407,116 @@ impl IntoIterator for Validators { } } -#[derive(Default, Debug, PartialEq)] -pub struct IntermediateRewards { - previous: BTreeMap<Address, u64>, - current: BTreeMap<Address, u64>, -} +pub mod v0 { + use std::mem; -impl IntermediateRewards { - pub fn load_from_state(state: &TopLevelState) -> StateResult<Self> { - let key = get_intermediate_rewards_key(); - let action_data = state.action_data(&key)?; - let (previous, current) = decode_map_tuple(action_data.as_ref()); + use super::*; - Ok(Self { - previous, - current, - }) + #[derive(Default, Debug, PartialEq)] + pub struct IntermediateRewards { + pub(super) previous: BTreeMap<Address, u64>, + pub(super) current: BTreeMap<Address, u64>, } - pub fn save_to_state(&self, state: &mut TopLevelState) -> StateResult<()> { - let key = get_intermediate_rewards_key(); - if self.previous.is_empty() && self.current.is_empty() { - state.remove_action_data(&key); - } else { - let encoded = encode_map_tuple(&self.previous, &self.current); - state.update_action_data(&key, encoded)?; + impl IntermediateRewards { + pub fn load_from_state(state: &TopLevelState) -> StateResult<Self> { + let key = get_intermediate_rewards_key(); + let action_data = state.action_data(&key)?; + let (previous, current) = decode_map_tuple(action_data.as_ref()); + + Ok(Self { + previous, + current, + }) } - Ok(()) - } - pub fn add_quantity(&mut self, address: Address, quantity: StakeQuantity) { - if quantity == 0 { - return + pub fn save_to_state(&self, state: &mut TopLevelState) -> StateResult<()> { + let key = get_intermediate_rewards_key(); + if self.previous.is_empty() && self.current.is_empty() { + state.remove_action_data(&key); + } else { + let encoded = encode_map_tuple(&self.previous, &self.current); + state.update_action_data(&key, encoded)?; + } + Ok(()) + } + + pub fn add_quantity(&mut self, address: Address, quantity: StakeQuantity) { + if quantity == 0 { + return + } + *self.current.entry(address).or_insert(0) += quantity; + } + + pub fn drain_previous(&mut self) -> BTreeMap<Address, u64> { + let mut new = BTreeMap::new(); + mem::swap(&mut new, &mut self.previous); + new + } + + pub fn move_current_to_previous(&mut self) { + assert!(self.previous.is_empty()); + mem::swap(&mut self.previous, &mut self.current); } - *self.current.entry(address).or_insert(0) += quantity; } +} + +pub mod v1 { + use std::mem; + + use super::*; - pub fn drain_previous(&mut self) -> BTreeMap<Address, u64> { - let mut new = BTreeMap::new(); - mem::swap(&mut new, &mut self.previous); - new + #[derive(Default, Debug, PartialEq)] + pub struct IntermediateRewards { + pub(super) current: BTreeMap<Address, u64>, + pub(super) calculated: BTreeMap<Address, u64>, } - pub fn move_current_to_previous(&mut self) { - assert!(self.previous.is_empty()); - mem::swap(&mut self.previous, &mut self.current); + impl IntermediateRewards { + pub fn load_from_state(state: &TopLevelState) -> StateResult<Self> { + let key = get_intermediate_rewards_key(); + let action_data = state.action_data(&key)?; + let (current, calculated) = decode_map_tuple(action_data.as_ref()); + + Ok(Self { + current, + calculated, + }) + } + + pub fn save_to_state(&self, state: &mut TopLevelState) -> StateResult<()> { + let key = get_intermediate_rewards_key(); + if self.current.is_empty() && self.calculated.is_empty() { + state.remove_action_data(&key); + } else { + let encoded = encode_map_tuple(&self.current, &self.calculated); + state.update_action_data(&key, encoded)?; + } + Ok(()) + } + + pub fn add_quantity(&mut self, address: Address, quantity: StakeQuantity) { + if quantity == 0 { + return + } + *self.current.entry(address).or_insert(0) += quantity; + } + + pub fn update_calculated(&mut self, rewards: BTreeMap<Address, u64>) { + self.calculated = rewards; + } + + pub fn drain_current(&mut self) -> BTreeMap<Address, u64> { + let mut new = BTreeMap::new(); + mem::swap(&mut new, &mut self.current); + new + } + + pub fn drain_calculated(&mut self) -> BTreeMap<Address, u64> { + let mut new = BTreeMap::new(); + mem::swap(&mut new, &mut self.calculated); + new + } } } @@ -1129,39 +1193,39 @@ mod tests { } #[test] - fn load_and_save_intermediate_rewards() { + fn load_and_save_intermediate_rewards_v0() { let mut state = helpers::get_temp_state(); - let rewards = IntermediateRewards::load_from_state(&state).unwrap(); + let rewards = v0::IntermediateRewards::load_from_state(&state).unwrap(); rewards.save_to_state(&mut state).unwrap(); } #[test] - fn add_quantity() { + fn add_quantity_v0() { let address1 = Address::random(); let address2 = Address::random(); let mut state = helpers::get_temp_state(); - let mut origin_rewards = IntermediateRewards::load_from_state(&state).unwrap(); + let mut origin_rewards = v0::IntermediateRewards::load_from_state(&state).unwrap(); origin_rewards.add_quantity(address1, 1); origin_rewards.add_quantity(address2, 2); origin_rewards.save_to_state(&mut state).unwrap(); - let recovered_rewards = IntermediateRewards::load_from_state(&state).unwrap(); + let recovered_rewards = v0::IntermediateRewards::load_from_state(&state).unwrap(); assert_eq!(origin_rewards, recovered_rewards); } #[test] - fn drain() { + fn drain_v0() { let address1 = Address::random(); let address2 = Address::random(); let mut state = helpers::get_temp_state(); - let mut origin_rewards = IntermediateRewards::load_from_state(&state).unwrap(); + let mut origin_rewards = v0::IntermediateRewards::load_from_state(&state).unwrap(); origin_rewards.add_quantity(address1, 1); origin_rewards.add_quantity(address2, 2); origin_rewards.save_to_state(&mut state).unwrap(); - let mut recovered_rewards = IntermediateRewards::load_from_state(&state).unwrap(); + let mut recovered_rewards = v0::IntermediateRewards::load_from_state(&state).unwrap(); assert_eq!(origin_rewards, recovered_rewards); let _drained = recovered_rewards.drain_previous(); recovered_rewards.save_to_state(&mut state).unwrap(); - let mut final_rewards = IntermediateRewards::load_from_state(&state).unwrap(); + let mut final_rewards = v0::IntermediateRewards::load_from_state(&state).unwrap(); assert_eq!(BTreeMap::new(), final_rewards.previous); let current = final_rewards.current.clone(); final_rewards.move_current_to_previous(); @@ -1169,6 +1233,59 @@ mod tests { assert_eq!(current, final_rewards.previous); } + #[test] + fn save_v0_and_load_v1_intermediate_rewards() { + let address1 = Address::random(); + let address2 = Address::random(); + let mut state = helpers::get_temp_state(); + let mut origin_rewards = v0::IntermediateRewards::load_from_state(&state).unwrap(); + origin_rewards.add_quantity(address1, 1); + origin_rewards.add_quantity(address2, 2); + origin_rewards.save_to_state(&mut state).unwrap(); + let recovered_rewards = v1::IntermediateRewards::load_from_state(&state).unwrap(); + assert_eq!(origin_rewards.previous, recovered_rewards.current); + assert_eq!(origin_rewards.current, recovered_rewards.calculated); + } + + #[test] + fn load_and_save_intermediate_rewards_v1() { + let mut state = helpers::get_temp_state(); + let rewards = v1::IntermediateRewards::load_from_state(&state).unwrap(); + rewards.save_to_state(&mut state).unwrap(); + } + + #[test] + fn add_quantity_v1() { + let address1 = Address::random(); + let address2 = Address::random(); + let mut state = helpers::get_temp_state(); + let mut origin_rewards = v1::IntermediateRewards::load_from_state(&state).unwrap(); + origin_rewards.add_quantity(address1, 1); + origin_rewards.add_quantity(address2, 2); + origin_rewards.save_to_state(&mut state).unwrap(); + let recovered_rewards = v1::IntermediateRewards::load_from_state(&state).unwrap(); + assert_eq!(origin_rewards, recovered_rewards); + } + + #[test] + fn drain_v1() { + let address1 = Address::random(); + let address2 = Address::random(); + let mut state = helpers::get_temp_state(); + let mut origin_rewards = v1::IntermediateRewards::load_from_state(&state).unwrap(); + origin_rewards.add_quantity(address1, 1); + origin_rewards.add_quantity(address2, 2); + origin_rewards.save_to_state(&mut state).unwrap(); + let mut recovered_rewards = v1::IntermediateRewards::load_from_state(&state).unwrap(); + assert_eq!(origin_rewards, recovered_rewards); + recovered_rewards.drain_current(); + recovered_rewards.save_to_state(&mut state).unwrap(); + let mut final_rewards = v1::IntermediateRewards::load_from_state(&state).unwrap(); + assert_eq!(BTreeMap::new(), final_rewards.current); + final_rewards.drain_calculated(); + assert_eq!(BTreeMap::new(), final_rewards.calculated); + } + #[test] fn candidates_deposit_add() { let mut state = helpers::get_temp_state(); diff --git a/core/src/consensus/stake/mod.rs b/core/src/consensus/stake/mod.rs index cf0a07022a..5134367362 100644 --- a/core/src/consensus/stake/mod.rs +++ b/core/src/consensus/stake/mod.rs @@ -34,7 +34,7 @@ use primitives::{Bytes, H256}; use rlp::{Decodable, Rlp}; pub use self::action_data::{Banned, Validator, Validators}; -use self::action_data::{Candidates, Delegation, IntermediateRewards, Jail, ReleaseResult, StakeAccount, Stakeholders}; +use self::action_data::{Candidates, Delegation, Jail, ReleaseResult, StakeAccount, Stakeholders}; pub use self::actions::Action; pub use self::distribute::fee_distribute; use super::ValidatorSet; @@ -156,13 +156,7 @@ impl ActionHandler for Stake { action.verify(current_params, client, validators) } - fn on_close_block( - &self, - _state: &mut TopLevelState, - _header: &Header, - _parent_header: &Header, - _parent_common_params: &CommonParams, - ) -> StateResult<()> { + fn on_close_block(&self, _state: &mut TopLevelState, _header: &Header) -> StateResult<()> { Ok(()) } } @@ -327,24 +321,61 @@ pub fn get_validators(state: &TopLevelState) -> StateResult<Validators> { Validators::load_from_state(state) } -pub fn add_intermediate_rewards(state: &mut TopLevelState, address: Address, reward: u64) -> StateResult<()> { - let mut rewards = IntermediateRewards::load_from_state(state)?; - rewards.add_quantity(address, reward); - rewards.save_to_state(state)?; - Ok(()) -} +pub mod v0 { + use super::action_data::v0::IntermediateRewards; + use super::*; -pub fn drain_previous_rewards(state: &mut TopLevelState) -> StateResult<BTreeMap<Address, u64>> { - let mut rewards = IntermediateRewards::load_from_state(state)?; - let drained = rewards.drain_previous(); - rewards.save_to_state(state)?; - Ok(drained) + pub fn add_intermediate_rewards(state: &mut TopLevelState, address: Address, reward: u64) -> StateResult<()> { + let mut rewards = IntermediateRewards::load_from_state(state)?; + rewards.add_quantity(address, reward); + rewards.save_to_state(state)?; + Ok(()) + } + + pub fn drain_previous_rewards(state: &mut TopLevelState) -> StateResult<BTreeMap<Address, u64>> { + let mut rewards = IntermediateRewards::load_from_state(state)?; + let drained = rewards.drain_previous(); + rewards.save_to_state(state)?; + Ok(drained) + } + + pub fn move_current_to_previous_intermediate_rewards(state: &mut TopLevelState) -> StateResult<()> { + let mut rewards = IntermediateRewards::load_from_state(state)?; + rewards.move_current_to_previous(); + rewards.save_to_state(state) + } } -pub fn move_current_to_previous_intermediate_rewards(state: &mut TopLevelState) -> StateResult<()> { - let mut rewards = IntermediateRewards::load_from_state(state)?; - rewards.move_current_to_previous(); - rewards.save_to_state(state) +pub mod v1 { + use super::action_data::v1::IntermediateRewards; + use super::*; + + pub fn add_intermediate_rewards(state: &mut TopLevelState, address: Address, reward: u64) -> StateResult<()> { + let mut rewards = IntermediateRewards::load_from_state(state)?; + rewards.add_quantity(address, reward); + rewards.save_to_state(state)?; + Ok(()) + } + + pub fn drain_current_rewards(state: &mut TopLevelState) -> StateResult<BTreeMap<Address, u64>> { + let mut rewards = IntermediateRewards::load_from_state(state)?; + let drained = rewards.drain_current(); + rewards.save_to_state(state)?; + Ok(drained) + } + + pub fn update_calculated_rewards(state: &mut TopLevelState, values: HashMap<Address, u64>) -> StateResult<()> { + let mut rewards = IntermediateRewards::load_from_state(state)?; + rewards.update_calculated(values.into_iter().collect()); + rewards.save_to_state(state) + } + + pub fn drain_calculated_rewards(state: &mut TopLevelState) -> StateResult<BTreeMap<Address, u64>> { + let mut rewards = IntermediateRewards::load_from_state(state)?; + let drained = rewards.drain_calculated(); + rewards.save_to_state(state)?; + Ok(drained) + } } pub fn update_validator_weights(state: &mut TopLevelState, block_author: &Address) -> StateResult<()> { diff --git a/core/src/consensus/tendermint/engine.rs b/core/src/consensus/tendermint/engine.rs index 3261e087a5..e17670ea11 100644 --- a/core/src/consensus/tendermint/engine.rs +++ b/core/src/consensus/tendermint/engine.rs @@ -24,8 +24,8 @@ use std::sync::{Arc, Weak}; use ckey::{public_to_address, Address}; use cnetwork::NetworkService; use crossbeam_channel as crossbeam; -use cstate::{ActionHandler, TopStateView}; -use ctypes::{BlockHash, BlockNumber, CommonParams, Header}; +use cstate::{ActionHandler, TopState, TopStateView}; +use ctypes::{BlockHash, CommonParams, Header}; use num_rational::Ratio; use super::super::stake; @@ -37,6 +37,7 @@ use super::worker; use super::{ChainNotify, Tendermint, SEAL_FIELDS}; use crate::account_provider::AccountProvider; use crate::block::*; +use crate::client::snapshot_notify::NotifySender as SnapshotNotifySender; use crate::client::{Client, ConsensusClient}; use crate::codechain_machine::CodeChainMachine; use crate::consensus::tendermint::params::TimeGapParams; @@ -134,17 +135,65 @@ impl ConsensusEngine for Tendermint { fn stop(&self) {} + /// Block transformation functions, before the transactions. + fn on_open_block(&self, block: &mut ExecutedBlock) -> Result<(), Error> { + let client = self.client().ok_or(EngineError::CannotOpenBlock)?; + + let block_number = block.header().number(); + let metadata = block.state().metadata()?.expect("Metadata must exist"); + let era = metadata.term_params().map_or(0, |p| p.era()); + if block_number == metadata.last_term_finished_block_num() + 1 { + match era { + 0 => {} + 1 => { + let rewards = stake::v1::drain_current_rewards(block.state_mut())?; + let start_of_the_current_term = block_number; + let start_of_the_previous_term = { + let end_of_the_two_level_previous_term = client + .last_term_finished_block_num((metadata.last_term_finished_block_num() - 1).into()) + .unwrap(); + + end_of_the_two_level_previous_term + 1 + }; + + let banned = stake::Banned::load_from_state(block.state())?; + let start_of_the_current_term_header = + encoded::Header::new(block.header().clone().rlp_bytes().to_vec()); + + let pending_rewards = calculate_pending_rewards_of_the_previous_term( + &*client, + &*self.validators, + rewards, + start_of_the_current_term, + start_of_the_current_term_header, + start_of_the_previous_term, + &banned, + )?; + + stake::v1::update_calculated_rewards(block.state_mut(), pending_rewards)?; + } + _ => unimplemented!(), + } + } + Ok(()) + } + fn on_close_block( &self, block: &mut ExecutedBlock, - parent_header: &Header, - parent_common_params: &CommonParams, term_common_params: Option<&CommonParams>, ) -> Result<(), Error> { + let client = self.client().ok_or(EngineError::CannotOpenBlock)?; + + let parent_hash = *block.header().parent_hash(); + let parent = client.block_header(&parent_hash.into()).expect("Parent header must exist").decode(); + let parent_common_params = client.common_params(parent_hash.into()).expect("CommonParams of parent must exist"); let author = *block.header().author(); + let block_number = block.header().number(); + let (total_reward, total_min_fee) = { let transactions = block.transactions(); - let block_reward = self.block_reward(block.header().number()); + let block_reward = self.block_reward(block_number); let total_min_fee: u64 = transactions.iter().map(|tx| tx.fee).sum(); let min_fee = transactions.iter().map(|tx| CodeChainMachine::min_cost(&parent_common_params, &tx.action)).sum(); @@ -160,90 +209,102 @@ impl ConsensusEngine for Tendermint { let block_author_reward = total_reward - total_min_fee + distributor.remaining_fee(); + let era = term_common_params.map_or(0, |p| p.era()); let metadata = block.state().metadata()?.expect("Metadata must exist"); - if metadata.current_term_id() == 0 { - self.machine.add_balance(block, &author, block_author_reward)?; - - if let Some(block_number) = - block_number_if_term_changed(block.header(), parent_header, parent_common_params) - { - // First term change - stake::on_term_close(block.state_mut(), block_number, &[])?; + let term = metadata.current_term_id(); + let term_seconds = match term { + 0 => parent_common_params.term_seconds(), + _ => term_common_params.expect("TermCommonParams should exist").term_seconds(), + }; + + match term { + 0 => { + self.machine.add_balance(block, &author, block_author_reward)?; + } + _ => { + stake::update_validator_weights(block.state_mut(), &author)?; + match era { + 0 => stake::v0::add_intermediate_rewards(block.state_mut(), author, block_author_reward)?, + 1 => stake::v1::add_intermediate_rewards(block.state_mut(), author, block_author_reward)?, + _ => unimplemented!(), + } } - return Ok(()) } - let block_author = *block.header().author(); - stake::update_validator_weights(&mut block.state_mut(), &block_author)?; - - stake::add_intermediate_rewards(block.state_mut(), author, block_author_reward)?; - - let term_common_params = term_common_params.expect("TermCommonParams should exist"); - let last_term_finished_block_num = if let Some(block_number) = - block_number_if_term_changed(block.header(), parent_header, term_common_params) - { - block_number - } else { + if !is_term_changed(block.header(), &parent, term_seconds) { return Ok(()) - }; - let rewards = stake::drain_previous_rewards(&mut block.state_mut())?; - - let start_of_the_current_term = metadata.last_term_finished_block_num() + 1; - let client = self - .client - .read() - .as_ref() - .ok_or(EngineError::CannotOpenBlock)? - .upgrade() - .ok_or(EngineError::CannotOpenBlock)?; - - let inactive_validators = if metadata.current_term_id() == 1 { - assert!(rewards.is_empty()); - - let validators = stake::Validators::load_from_state(block.state())? - .into_iter() - .map(|val| public_to_address(val.pubkey())) - .collect(); - inactive_validators(&*client, start_of_the_current_term, block.header(), validators) - } else { - let start_of_the_previous_term = { - let end_of_the_two_level_previous_term = - client.last_term_finished_block_num((metadata.last_term_finished_block_num() - 1).into()).unwrap(); - - end_of_the_two_level_previous_term + 1 - }; - - let banned = stake::Banned::load_from_state(block.state())?; - let start_of_the_current_term_header = if block.header().number() == start_of_the_current_term { - encoded::Header::new(block.header().clone().rlp_bytes().to_vec()) - } else { - client.block_header(&start_of_the_current_term.into()).unwrap() - }; - - let pending_rewards = calculate_pending_rewards_of_the_previous_term( - &*client, - &*self.validators, - rewards, - start_of_the_current_term, - start_of_the_current_term_header, - start_of_the_previous_term, - &banned, - )?; - - for (address, reward) in pending_rewards { - self.machine.add_balance(block, &address, reward)?; - } + } - let validators = stake::Validators::load_from_state(block.state())? - .into_iter() - .map(|val| public_to_address(val.pubkey())) - .collect(); - inactive_validators(&*client, start_of_the_current_term, block.header(), validators) + let inactive_validators = match (era, term) { + (0, 0) => Vec::new(), + (0, _) => { + let rewards = stake::v0::drain_previous_rewards(block.state_mut())?; + let start_of_the_current_term = metadata.last_term_finished_block_num() + 1; + + if term > 1 { + let start_of_the_previous_term = { + let end_of_the_two_level_previous_term = client + .last_term_finished_block_num((metadata.last_term_finished_block_num() - 1).into()) + .unwrap(); + + end_of_the_two_level_previous_term + 1 + }; + + let banned = stake::Banned::load_from_state(block.state())?; + let start_of_the_current_term_header = if block_number == start_of_the_current_term { + encoded::Header::new(block.header().clone().rlp_bytes().to_vec()) + } else { + client.block_header(&start_of_the_current_term.into()).unwrap() + }; + + let pending_rewards = calculate_pending_rewards_of_the_previous_term( + &*client, + &*self.validators, + rewards, + start_of_the_current_term, + start_of_the_current_term_header, + start_of_the_previous_term, + &banned, + )?; + + for (address, reward) in pending_rewards { + self.machine.add_balance(block, &address, reward)?; + } + } + + stake::v0::move_current_to_previous_intermediate_rewards(block.state_mut())?; + + let validators = stake::Validators::load_from_state(block.state())? + .into_iter() + .map(|val| public_to_address(val.pubkey())) + .collect(); + inactive_validators(&*client, start_of_the_current_term, block.header(), validators) + } + (1, _) => { + for (address, reward) in stake::v1::drain_calculated_rewards(block.state_mut())? { + self.machine.add_balance(block, &address, reward)?; + } + + let start_of_the_current_term = metadata.last_term_finished_block_num() + 1; + let validators = stake::Validators::load_from_state(block.state())? + .into_iter() + .map(|val| public_to_address(val.pubkey())) + .collect(); + inactive_validators(&*client, start_of_the_current_term, block.header(), validators) + } + _ => unimplemented!(), }; - stake::move_current_to_previous_intermediate_rewards(&mut block.state_mut())?; - stake::on_term_close(block.state_mut(), last_term_finished_block_num, &inactive_validators)?; + stake::on_term_close(block.state_mut(), block_number, &inactive_validators)?; + match term { + 0 => {} + _ => match metadata.params().map_or(0, |p| p.era()) { + 0 => {} + 1 => block.state_mut().snapshot_term_params()?, + _ => unimplemented!("It is not decided how we handle this"), + }, + } Ok(()) } @@ -279,12 +340,8 @@ impl ConsensusEngine for Tendermint { let inner = self.inner.clone(); let extension = service.register_extension(move |api| TendermintExtension::new(inner, timeouts, api)); - let client = Weak::clone(self.client.read().as_ref().unwrap()); + let client = Arc::downgrade(&self.client().unwrap()); self.extension_initializer.send((extension, client)).unwrap(); - - let (result, receiver) = crossbeam::bounded(1); - self.inner.send(worker::Event::Restore(result)).unwrap(); - receiver.recv().unwrap(); } fn register_time_gap_config_to_worker(&self, time_gap_params: TimeGapParams) { @@ -303,6 +360,16 @@ impl ConsensusEngine for Tendermint { client.add_notify(Arc::downgrade(&self.chain_notify) as Weak<dyn ChainNotify>); } + fn register_snapshot_notify_sender(&self, sender: SnapshotNotifySender) { + self.snapshot_notify_sender_initializer.send(sender).unwrap(); + } + + fn register_is_done(&self) { + let (result, receiver) = crossbeam::bounded(1); + self.inner.send(worker::Event::Restore(result)).unwrap(); + receiver.recv().unwrap(); + } + fn get_best_block_from_best_proposal_header(&self, header: &HeaderView) -> BlockHash { header.parent_hash() } @@ -323,13 +390,7 @@ impl ConsensusEngine for Tendermint { } fn possible_authors(&self, block_number: Option<u64>) -> Result<Option<Vec<Address>>, EngineError> { - let client = self - .client - .read() - .as_ref() - .ok_or(EngineError::CannotOpenBlock)? - .upgrade() - .ok_or(EngineError::CannotOpenBlock)?; + let client = self.client().ok_or(EngineError::CannotOpenBlock)?; let block_hash = match block_number { None => { client.block_header(&BlockId::Latest).expect("latest block must exist").hash() // the latest block @@ -343,22 +404,15 @@ impl ConsensusEngine for Tendermint { } } -fn block_number_if_term_changed( - header: &Header, - parent_header: &Header, - common_params: &CommonParams, -) -> Option<BlockNumber> { - let term_seconds = common_params.term_seconds(); +pub(crate) fn is_term_changed(header: &Header, parent: &Header, term_seconds: u64) -> bool { if term_seconds == 0 { - return None + return false } let current_term_period = header.timestamp() / term_seconds; - let parent_term_period = parent_header.timestamp() / term_seconds; - if current_term_period == parent_term_period { - return None - } - Some(header.number()) + let parent_term_period = parent.timestamp() / term_seconds; + + current_term_period != parent_term_period } fn inactive_validators( diff --git a/core/src/consensus/tendermint/mod.rs b/core/src/consensus/tendermint/mod.rs index 85edcd6a9a..13cd8a4e15 100644 --- a/core/src/consensus/tendermint/mod.rs +++ b/core/src/consensus/tendermint/mod.rs @@ -41,6 +41,7 @@ pub use self::types::{Height, Step, View}; use super::{stake, ValidatorSet}; use crate::client::ConsensusClient; use crate::codechain_machine::CodeChainMachine; +use crate::snapshot_notify::NotifySender as SnapshotNotifySender; use crate::ChainNotify; /// Timer token representing the consensus step timeouts. @@ -58,6 +59,7 @@ pub struct Tendermint { client: RwLock<Option<Weak<dyn ConsensusClient>>>, external_params_initializer: crossbeam::Sender<TimeGapParams>, extension_initializer: crossbeam::Sender<(crossbeam::Sender<network::Event>, Weak<dyn ConsensusClient>)>, + snapshot_notify_sender_initializer: crossbeam::Sender<SnapshotNotifySender>, timeouts: TimeoutParams, join: Option<JoinHandle<()>>, quit_tendermint: crossbeam::Sender<()>, @@ -93,8 +95,14 @@ impl Tendermint { let timeouts = our_params.timeouts; let machine = Arc::new(machine); - let (join, external_params_initializer, extension_initializer, inner, quit_tendermint) = - worker::spawn(our_params.validators); + let ( + join, + external_params_initializer, + extension_initializer, + snapshot_notify_sender_initializer, + inner, + quit_tendermint, + ) = worker::spawn(our_params.validators); let action_handlers: Vec<Arc<dyn ActionHandler>> = vec![stake.clone()]; let chain_notify = Arc::new(TendermintChainNotify::new(inner.clone())); @@ -102,6 +110,7 @@ impl Tendermint { client: Default::default(), external_params_initializer, extension_initializer, + snapshot_notify_sender_initializer, timeouts, join: Some(join), quit_tendermint, @@ -115,6 +124,10 @@ impl Tendermint { has_signer: false.into(), }) } + + fn client(&self) -> Option<Arc<dyn ConsensusClient>> { + self.client.read().as_ref()?.upgrade() + } } const SEAL_FIELDS: usize = 4; @@ -157,9 +170,8 @@ mod tests { let genesis_header = scheme.genesis_header(); let b = OpenBlock::try_new(scheme.engine.as_ref(), db, &genesis_header, proposer, vec![]).unwrap(); let seal = scheme.engine.generate_seal(None, &genesis_header).seal_fields().unwrap(); - let common_params = CommonParams::default_for_test(); let term_common_params = CommonParams::default_for_test(); - let b = b.close(&genesis_header, &common_params, Some(&term_common_params)).unwrap(); + let b = b.close(&genesis_header, Some(&term_common_params)).unwrap(); (b, seal) } diff --git a/core/src/consensus/tendermint/worker.rs b/core/src/consensus/tendermint/worker.rs index aec882b908..f9abe27922 100644 --- a/core/src/consensus/tendermint/worker.rs +++ b/core/src/consensus/tendermint/worker.rs @@ -50,7 +50,9 @@ use crate::consensus::validator_set::{DynamicValidator, ValidatorSet}; use crate::consensus::{EngineError, Seal}; use crate::encoded; use crate::error::{BlockError, Error}; +use crate::snapshot_notify::NotifySender as SnapshotNotifySender; use crate::transaction::{SignedTransaction, UnverifiedTransaction}; +use crate::types::BlockStatus; use crate::views::BlockView; use crate::BlockId; use std::cell::Cell; @@ -59,6 +61,7 @@ type SpawnResult = ( JoinHandle<()>, crossbeam::Sender<TimeGapParams>, crossbeam::Sender<(crossbeam::Sender<network::Event>, Weak<dyn ConsensusClient>)>, + crossbeam::Sender<SnapshotNotifySender>, crossbeam::Sender<Event>, crossbeam::Sender<()>, ); @@ -97,6 +100,7 @@ struct Worker { time_gap_params: TimeGapParams, timeout_token_nonce: usize, vote_regression_checker: VoteRegressionChecker, + snapshot_notify_sender: SnapshotNotifySender, } pub enum Event { @@ -180,6 +184,7 @@ impl Worker { extension: EventSender<network::Event>, client: Weak<dyn ConsensusClient>, time_gap_params: TimeGapParams, + snapshot_notify_sender: SnapshotNotifySender, ) -> Self { Worker { client, @@ -198,6 +203,7 @@ impl Worker { time_gap_params, timeout_token_nonce: ENGINE_TIMEOUT_TOKEN_NONCE_BASE, vote_regression_checker: VoteRegressionChecker::new(), + snapshot_notify_sender, } } @@ -206,6 +212,7 @@ impl Worker { let (quit, quit_receiver) = crossbeam::bounded(1); let (external_params_initializer, external_params_receiver) = crossbeam::bounded(1); let (extension_initializer, extension_receiver) = crossbeam::bounded(1); + let (snapshot_notify_sender_initializer, snapshot_notify_sender_receiver) = crossbeam::bounded(1); let join = Builder::new() .name("tendermint".to_string()) .spawn(move || { @@ -249,8 +256,29 @@ impl Worker { return } }; + // TODO: Make initialization steps to order insensitive. + let snapshot_notify_sender = crossbeam::select! { + recv(snapshot_notify_sender_receiver) -> msg => { + match msg { + Ok(sender) => sender, + Err(crossbeam::RecvError) => { + cerror!(ENGINE, "The tendermint extension is not initalized."); + return + } + } + } + recv(quit_receiver) -> msg => { + match msg { + Ok(()) => {}, + Err(crossbeam::RecvError) => { + cerror!(ENGINE, "The quit channel for tendermint thread had been closed."); + } + } + return + } + }; validators.register_client(Weak::clone(&client)); - let mut inner = Self::new(validators, extension, client, time_gap_params); + let mut inner = Self::new(validators, extension, client, time_gap_params, snapshot_notify_sender); loop { crossbeam::select! { recv(receiver) -> msg => { @@ -374,7 +402,7 @@ impl Worker { } }) .unwrap(); - (join, external_params_initializer, extension_initializer, sender, quit) + (join, external_params_initializer, extension_initializer, snapshot_notify_sender_initializer, sender, quit) } /// The client is a thread-safe struct. Using it in multi-threads is safe. @@ -934,7 +962,8 @@ impl Worker { } fn on_imported_proposal(&mut self, proposal: &Header) { - if proposal.number() < 1 { + // NOTE: Only the genesis block and the snapshot target don't have the parent in the blockchain + if self.client().block_status(&BlockId::Hash(*proposal.parent_hash())) == BlockStatus::Unknown { return } @@ -1616,6 +1645,8 @@ impl Worker { } }; + self.send_snapshot_notify(c.as_ref(), enacted.as_slice()); + if self.step.is_commit() && (imported.len() + enacted.len() == 1) { let (_, committed_block_hash) = self.step.committed().expect("Commit state always has block_hash"); if imported.first() == Some(&committed_block_hash) { @@ -1667,6 +1698,26 @@ impl Worker { } } + // Notify once for the latest block even if multiple blocks have been enacted. + fn send_snapshot_notify(&mut self, c: &dyn ConsensusClient, enacted: &[BlockHash]) { + let mut last_snapshot_point = None; + for block_hash in enacted.iter().rev() { + let block_id = BlockId::Hash(*block_hash); + let last_term_finished_block_num = c.last_term_finished_block_num(block_id).expect("Block is enacted"); + let block_number = c.block_number(&block_id).expect("Block number should exist for enacted block"); + + if let Some(params) = c.term_common_params(block_id) { + if params.era() == 1 && (last_term_finished_block_num + 1 == block_number) { + last_snapshot_point = Some(block_hash); + } + } + } + if let Some(last_snapshot_point) = last_snapshot_point { + // TODO: Reduce the snapshot frequency. + self.snapshot_notify_sender.notify(*last_snapshot_point); + } + } + fn send_proposal_block( &self, signature: SchnorrSignature, diff --git a/core/src/lib.rs b/core/src/lib.rs index db2d091932..c0ada218b2 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -84,10 +84,11 @@ mod tests; pub use crate::account_provider::{AccountProvider, Error as AccountProviderError}; pub use crate::block::Block; +pub use crate::client::snapshot_notify; pub use crate::client::{ AccountData, AssetClient, BlockChainClient, BlockChainTrait, ChainNotify, Client, ClientConfig, DatabaseClient, - EngineClient, EngineInfo, ExecuteClient, ImportBlock, MiningBlockChainClient, Shard, StateInfo, TermInfo, - TestBlockChainClient, TextClient, + EngineClient, EngineInfo, ExecuteClient, ImportBlock, MiningBlockChainClient, Shard, SnapshotClient, StateInfo, + TermInfo, TestBlockChainClient, TextClient, }; pub use crate::consensus::{EngineType, TimeGapParams}; pub use crate::db::{COL_STATE, NUM_COLUMNS}; @@ -98,4 +99,4 @@ pub use crate::service::ClientService; pub use crate::transaction::{ LocalizedTransaction, PendingSignedTransactions, SignedTransaction, UnverifiedTransaction, }; -pub use crate::types::{BlockId, TransactionId}; +pub use crate::types::{BlockId, BlockStatus, TransactionId}; diff --git a/core/src/miner/miner.rs b/core/src/miner/miner.rs index 8c7c446262..5b7c42d865 100644 --- a/core/src/miner/miner.rs +++ b/core/src/miner/miner.rs @@ -527,6 +527,7 @@ impl Miner { return Ok(None) } } + self.engine.on_open_block(open_block.inner_mut())?; let mut invalid_transactions = Vec::new(); @@ -603,18 +604,8 @@ impl Miner { let parent_header = chain.block_header(&parent_hash.into()).expect("Parent header MUST exist"); (parent_header.decode(), parent_hash) }; - let parent_common_params = chain.common_params(parent_hash.into()).unwrap(); - let term_common_params = { - let block_number = chain - .last_term_finished_block_num(parent_hash.into()) - .expect("The block of the parent hash should exist"); - if block_number == 0 { - None - } else { - Some(chain.common_params((block_number).into()).expect("Common params should exist")) - } - }; - let block = open_block.close(&parent_header, &parent_common_params, term_common_params.as_ref())?; + let term_common_params = chain.term_common_params(parent_hash.into()); + let block = open_block.close(&parent_header, term_common_params.as_ref())?; let fetch_seq = |p: &Public| { let address = public_to_address(p); diff --git a/core/src/miner/sealing_queue.rs b/core/src/miner/sealing_queue.rs index 3866274d04..16f282367e 100644 --- a/core/src/miner/sealing_queue.rs +++ b/core/src/miner/sealing_queue.rs @@ -87,9 +87,8 @@ mod tests { let genesis_header = scheme.genesis_header(); let db = scheme.ensure_genesis_state(get_temp_state_db()).unwrap(); let b = OpenBlock::try_new(&*scheme.engine, db, &genesis_header, address, vec![]).unwrap(); - let common_params = CommonParams::default_for_test(); let term_common_params = CommonParams::default_for_test(); - b.close(&genesis_header, &common_params, Some(&term_common_params)).unwrap() + b.close(&genesis_header, Some(&term_common_params)).unwrap() } #[test] diff --git a/rpc/src/v1/errors.rs b/rpc/src/v1/errors.rs index 2dc961786b..eaf98cf5d4 100644 --- a/rpc/src/v1/errors.rs +++ b/rpc/src/v1/errors.rs @@ -294,6 +294,14 @@ pub fn invalid_custom_action(err: String) -> Error { } } +pub fn io(error: std::io::Error) -> Error { + Error { + code: ErrorCode::InternalError, + message: format!("{}", error), + data: None, + } +} + /// Internal error signifying a logic error in code. /// Should not be used when function can just fail /// because of invalid parameters or incomplete node state. diff --git a/rpc/src/v1/impls/devel.rs b/rpc/src/v1/impls/devel.rs index 588b4f8f83..75af7f3ffe 100644 --- a/rpc/src/v1/impls/devel.rs +++ b/rpc/src/v1/impls/devel.rs @@ -23,7 +23,7 @@ use std::vec::Vec; use ccore::{ BlockId, DatabaseClient, EngineClient, EngineInfo, MinerService, MiningBlockChainClient, SignedTransaction, - TermInfo, COL_STATE, + SnapshotClient, TermInfo, COL_STATE, }; use ccrypto::Blake; use cjson::bytes::Bytes; @@ -33,7 +33,7 @@ use csync::BlockSyncEvent; use ctypes::transaction::{ Action, AssetMintOutput, AssetOutPoint, AssetTransferInput, AssetTransferOutput, Transaction, }; -use ctypes::{Tracker, TxHash}; +use ctypes::{BlockHash, Tracker, TxHash}; use jsonrpc_core::Result; use kvdb::KeyValueDB; use primitives::{H160, H256}; @@ -70,7 +70,7 @@ where impl<C, M> Devel for DevelClient<C, M> where - C: DatabaseClient + EngineInfo + EngineClient + MiningBlockChainClient + TermInfo + 'static, + C: DatabaseClient + EngineInfo + EngineClient + MiningBlockChainClient + TermInfo + SnapshotClient + 'static, M: MinerService + 'static, { fn get_state_trie_keys(&self, offset: usize, limit: usize) -> Result<Vec<H256>> { @@ -108,6 +108,11 @@ where } } + fn snapshot(&self, block_hash: BlockHash) -> Result<()> { + self.client.notify_snapshot(BlockId::Hash(block_hash)); + Ok(()) + } + fn test_tps(&self, setting: TPSTestSetting) -> Result<f64> { let common_params = self.client.common_params(BlockId::Latest).unwrap(); let mint_fee = common_params.min_asset_mint_cost(); diff --git a/rpc/src/v1/impls/mod.rs b/rpc/src/v1/impls/mod.rs index 45e7678459..3360f5682e 100644 --- a/rpc/src/v1/impls/mod.rs +++ b/rpc/src/v1/impls/mod.rs @@ -21,6 +21,7 @@ mod engine; mod mempool; mod miner; mod net; +mod snapshot; pub use self::account::AccountClient; pub use self::chain::ChainClient; @@ -29,3 +30,4 @@ pub use self::engine::EngineClient; pub use self::mempool::MempoolClient; pub use self::miner::MinerClient; pub use self::net::NetClient; +pub use self::snapshot::SnapshotClient; diff --git a/rpc/src/v1/impls/snapshot.rs b/rpc/src/v1/impls/snapshot.rs new file mode 100644 index 0000000000..f030c9bbd3 --- /dev/null +++ b/rpc/src/v1/impls/snapshot.rs @@ -0,0 +1,88 @@ +// Copyright 2018-2019 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use std::fs; +use std::str::FromStr; +use std::sync::Arc; + +use ccore::{BlockChainClient, BlockId}; +use ctypes::BlockHash; +use primitives::H256; + +use jsonrpc_core::Result; + +use super::super::errors; +use super::super::traits::Snapshot; +use super::super::types::BlockNumberAndHash; + +pub struct SnapshotClient<C> +where + C: BlockChainClient, { + client: Arc<C>, + snapshot_path: Option<String>, +} + +impl<C> SnapshotClient<C> +where + C: BlockChainClient, +{ + pub fn new(client: Arc<C>, snapshot_path: Option<String>) -> Self { + SnapshotClient { + client, + snapshot_path, + } + } +} + +impl<C> Snapshot for SnapshotClient<C> +where + C: BlockChainClient + 'static, +{ + fn get_snapshot_list(&self) -> Result<Vec<BlockNumberAndHash>> { + if let Some(snapshot_path) = &self.snapshot_path { + let mut result = Vec::new(); + for entry in fs::read_dir(snapshot_path).map_err(errors::io)? { + let entry = entry.map_err(errors::io)?; + + // Check if the entry is a directory + let file_type = entry.file_type().map_err(errors::io)?; + if !file_type.is_dir() { + continue + } + + let path = entry.path(); + let name = match path.file_name().expect("Directories always have file name").to_str() { + Some(n) => n, + None => continue, + }; + let hash = match H256::from_str(name) { + Ok(h) => BlockHash::from(h), + Err(_) => continue, + }; + if let Some(number) = self.client.block_number(&BlockId::Hash(hash)) { + result.push(BlockNumberAndHash { + number, + hash, + }); + } + } + result.sort_unstable_by(|a, b| b.number.cmp(&a.number)); + Ok(result) + } else { + Ok(Vec::new()) + } + } +} diff --git a/rpc/src/v1/traits/devel.rs b/rpc/src/v1/traits/devel.rs index e8604e910e..565a331976 100644 --- a/rpc/src/v1/traits/devel.rs +++ b/rpc/src/v1/traits/devel.rs @@ -17,6 +17,7 @@ use std::net::SocketAddr; use cjson::bytes::Bytes; +use ctypes::BlockHash; use jsonrpc_core::Result; use primitives::H256; @@ -39,6 +40,9 @@ pub trait Devel { #[rpc(name = "devel_getBlockSyncPeers")] fn get_block_sync_peers(&self) -> Result<Vec<SocketAddr>>; + #[rpc(name = "devel_snapshot")] + fn snapshot(&self, hash: BlockHash) -> Result<()>; + #[rpc(name = "devel_testTPS")] fn test_tps(&self, setting: TPSTestSetting) -> Result<f64>; } diff --git a/rpc/src/v1/traits/mod.rs b/rpc/src/v1/traits/mod.rs index 719f186e49..7f2bd04599 100644 --- a/rpc/src/v1/traits/mod.rs +++ b/rpc/src/v1/traits/mod.rs @@ -21,6 +21,7 @@ mod engine; mod mempool; mod miner; mod net; +mod snapshot; pub use self::account::Account; pub use self::chain::Chain; @@ -29,3 +30,4 @@ pub use self::engine::Engine; pub use self::mempool::Mempool; pub use self::miner::Miner; pub use self::net::Net; +pub use self::snapshot::Snapshot; diff --git a/rpc/src/v1/traits/snapshot.rs b/rpc/src/v1/traits/snapshot.rs new file mode 100644 index 0000000000..0fd9c18366 --- /dev/null +++ b/rpc/src/v1/traits/snapshot.rs @@ -0,0 +1,26 @@ +// Copyright 2018-2019 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use jsonrpc_core::Result; + +use super::super::types::BlockNumberAndHash; + +#[rpc(server)] +pub trait Snapshot { + /// Gets list of block numbers and block hashes of the snapshots. + #[rpc(name = "snapshot_getList")] + fn get_snapshot_list(&self) -> Result<Vec<BlockNumberAndHash>>; +} diff --git a/spec/Block-Synchronization-Extension.md b/spec/Block-Synchronization-Extension.md index fb5a106f75..aa6f3f0372 100644 --- a/spec/Block-Synchronization-Extension.md +++ b/spec/Block-Synchronization-Extension.md @@ -19,13 +19,14 @@ Message := ### Status ``` -Status(total_score, best_hash, genesis_hash) +Status(nonce, best_hash, genesis_hash) ``` Send current chain status to peer. * Identifier: 0x01 -* Restriction: None +* Restriction: + * `nonce` SHOULD be monotonically increasing every time the message is sent. ## Request messages @@ -53,32 +54,17 @@ Request corresponding bodies for each hash. * Restriction: * MUST include at least one item - -### GetStateHead - -``` -GetStateHead(block_hash) -``` - -Request corresponding state head for block of `block_hash`. - -* Identifier: 0x06 -* Restriction: Block number of requested block MUST be multiple of 214. - - ### GetStateChunk ``` -GetStateChunk(block_hash, tree_root) +GetStateChunk(block_hash, [...chunk_roots]) ``` -Request entire subtree starting from `tree_root`. +Request corresponding snapshot chunk for each `chunk_root`. -* Identifier: 0x08 +* Identifier: 0x0a * Restriction: - * Block number of requested block MUST be multiple of 214. - * `tree_root` MUST be included in requested block’s state trie. - * Depth of `tree_root` inside state trie MUST be equal to 2. (Depth of state root is 0) + * All values in `[...chunk_roots]` MUST be included in requested block’s state trie. ## Response messages @@ -113,30 +99,15 @@ Response to `GetBodies` message. Snappy algorithm is used to compress content. * If received body is zero-length array, it means either body value is [], or sender doesn’t have body for requested hash -### StateHead - -``` -StateHead(compressed((key_0, value_0), …) | []) -``` - -Response to `GetStateHead` message. Key and value included in this messages are raw value stored in state trie. Snappy algorithm is used for compression of content. - -* Identifier: 0x07 -* Restriction: - * State root of requested block MUST be included - * For all nodes with depth of less than 2 included in this message, all of its child MUST also be included. - * Content MUST be empty array if sender didn’t have requested data - - ### StateChunk ``` -StateChunk(compressed((key_0, value_0), …) | []) +StateChunk([compressed([terminal_0, …] | []), ...]) ``` -Response to `GetStateChunk` message. Details of message is same as `StateHead` message. +Response to `GetStateChunk` message. Snappy algorithm is used for compression of content. -* Identifier: 0x09 +* Identifier: 0x0b * Restriction: - * Node corresponding to tree_root in request MUST be included - * Every nodes included in message MUST have all of its child in same message. - * Content MUST be empty array if sender didn’t have requested data + * Number and order of chunks included in this message MUST be equal to request information. + * Node corresponding to `chunk_root` in request MUST be included + * If sender doesn’t have a chunk for the requested hash, corresponding chunk MUST be `[]`(uncompressed), not omitted. diff --git a/spec/JSON-RPC.md b/spec/JSON-RPC.md index cefee266dc..870192c22b 100644 --- a/spec/JSON-RPC.md +++ b/spec/JSON-RPC.md @@ -367,6 +367,7 @@ When `Transaction` is included in any response, there will be an additional fiel *** * [devel_getStateTrieKeys](#devel_getstatetriekeys) * [devel_getStateTrieValue](#devel_getstatetrievalue) + * [devel_snapshot](#devel_snapshot) * [devel_startSealing](#devel_startsealing) * [devel_stopSealing](#devel_stopsealing) * [devel_getBlockSyncPeers](#devel_getblocksyncpeers) @@ -2979,6 +2980,33 @@ Gets the value of the state trie with the given key. [Back to **List of methods**](#list-of-methods) +## devel_snapshot +Snapshot the state of the given block hash. + +### Params + 1. key: `H256` + +### Returns + +### Request Example +``` + curl \ + -H 'Content-Type: application/json' \ + -d '{"jsonrpc": "2.0", "method": "devel_snapshot", "params": ["0xfc196ede542b03b55aee9f106004e7e3d7ea6a9600692e964b4735a260356b50"], "id": null}' \ + localhost:8080 +``` + +### Response Example +``` +{ + "jsonrpc":"2.0", + "result":[], + "id":null +} +``` + +[Back to **List of methods**](#list-of-methods) + ## devel_startSealing Starts and enables sealing blocks by the miner. diff --git a/state/src/action_handler/hit.rs b/state/src/action_handler/hit.rs index 407524fe1f..7891b99e36 100644 --- a/state/src/action_handler/hit.rs +++ b/state/src/action_handler/hit.rs @@ -86,13 +86,7 @@ impl ActionHandler for HitHandler { Ok(()) } - fn on_close_block( - &self, - state: &mut TopLevelState, - _header: &Header, - _parent_header: &Header, - _parent_common_params: &CommonParams, - ) -> StateResult<()> { + fn on_close_block(&self, state: &mut TopLevelState, _header: &Header) -> StateResult<()> { let address = self.close_count(); let action_data = state.action_data(&address)?.unwrap_or_default(); let prev_counter: u32 = rlp::decode(&*action_data).unwrap(); diff --git a/state/src/action_handler/mod.rs b/state/src/action_handler/mod.rs index 519f8e62df..dd7c1da1ac 100644 --- a/state/src/action_handler/mod.rs +++ b/state/src/action_handler/mod.rs @@ -47,13 +47,7 @@ pub trait ActionHandler: Send + Sync { Ok(some_action_data) } - fn on_close_block( - &self, - state: &mut TopLevelState, - header: &Header, - parent_header: &Header, - parent_common_params: &CommonParams, - ) -> StateResult<()>; + fn on_close_block(&self, state: &mut TopLevelState, header: &Header) -> StateResult<()>; } pub trait FindActionHandler { diff --git a/state/src/impls/top_level.rs b/state/src/impls/top_level.rs index b871efaa58..4d51ec0b3f 100644 --- a/state/src/impls/top_level.rs +++ b/state/src/impls/top_level.rs @@ -999,6 +999,12 @@ impl TopState for TopLevelState { metadata.increase_seq(); Ok(()) } + + fn snapshot_term_params(&mut self) -> StateResult<()> { + let mut metadata = self.get_metadata_mut()?; + metadata.snapshot_term_params(); + Ok(()) + } } fn is_active_account(state: &dyn TopStateView, address: &Address) -> TrieResult<bool> { diff --git a/state/src/item/metadata.rs b/state/src/item/metadata.rs index 6ad74076b3..39651888a4 100644 --- a/state/src/item/metadata.rs +++ b/state/src/item/metadata.rs @@ -34,6 +34,7 @@ pub struct Metadata { term: TermMetadata, seq: u64, params: Option<CommonParams>, + term_params: Option<CommonParams>, } impl Metadata { @@ -45,6 +46,7 @@ impl Metadata { term: Default::default(), seq: 0, params: None, + term_params: None, } } @@ -93,6 +95,14 @@ impl Metadata { self.params = Some(params); } + pub fn term_params(&self) -> Option<&CommonParams> { + self.term_params.as_ref() + } + + pub fn snapshot_term_params(&mut self) { + self.term_params = self.params; + } + pub fn increase_term_id(&mut self, last_term_finished_block_num: u64) { assert!(self.term.last_term_finished_block_num < last_term_finished_block_num); self.term.last_term_finished_block_num = last_term_finished_block_num; @@ -124,25 +134,31 @@ impl CacheableItem for Metadata { const PREFIX: u8 = super::METADATA_PREFIX; +const INITIAL_LEN: usize = 4; +const TERM_LEN: usize = INITIAL_LEN + 2; +const PARAMS_LEN: usize = TERM_LEN + 2; +const TERM_PARAMS_LEN: usize = PARAMS_LEN + 1; +const VALID_LEN: &[usize] = &[INITIAL_LEN, TERM_LEN, PARAMS_LEN, TERM_PARAMS_LEN]; + impl Encodable for Metadata { fn rlp_append(&self, s: &mut RlpStream) { - const INITIAL_LEN: usize = 4; - const TERM_LEN: usize = 2; - const PARAMS_LEN: usize = 2; - let mut len = INITIAL_LEN; - let term_changed = self.term != Default::default(); - if term_changed { - len += TERM_LEN; - } - let params_changed = self.seq != 0; - if params_changed { - if !term_changed { - len += TERM_LEN; + let term_params_exist = self.term_params.is_some(); + + let len = if term_params_exist { + if !params_changed { + panic!("Term params only can be changed if params changed"); } - len += PARAMS_LEN; - } + TERM_PARAMS_LEN + } else if params_changed { + PARAMS_LEN + } else if term_changed { + TERM_LEN + } else { + INITIAL_LEN + }; + s.begin_list(len) .append(&PREFIX) .append(&self.number_of_shards) @@ -159,48 +175,63 @@ impl Encodable for Metadata { } s.append(&self.seq).append(self.params.as_ref().unwrap()); } + if term_params_exist { + if !params_changed { + unreachable!("Term params only can be changed if params changed"); + } + s.append(self.term_params.as_ref().unwrap()); + } } } impl Decodable for Metadata { fn decode(rlp: &Rlp) -> Result<Self, DecoderError> { - let (term, seq, params) = match rlp.item_count()? { - 4 => (TermMetadata::default(), 0, None), - 6 => ( - TermMetadata { - last_term_finished_block_num: rlp.val_at(4)?, - current_term_id: rlp.val_at(5)?, - }, - 0, - None, - ), - 8 => ( - TermMetadata { - last_term_finished_block_num: rlp.val_at(4)?, - current_term_id: rlp.val_at(5)?, - }, - rlp.val_at(6)?, - Some(rlp.val_at(7)?), - ), - item_count => { - return Err(DecoderError::RlpInvalidLength { - got: item_count, - expected: 4, - }) - } - }; + let item_count = rlp.item_count()?; + if !VALID_LEN.contains(&item_count) { + return Err(DecoderError::RlpInvalidLength { + got: item_count, + expected: 4, + }) + } + let prefix = rlp.val_at::<u8>(0)?; if PREFIX != prefix { cdebug!(STATE, "{} is not an expected prefix for asset", prefix); return Err(DecoderError::Custom("Unexpected prefix")) } + let number_of_shards = rlp.val_at(1)?; + let number_of_initial_shards = rlp.val_at(2)?; + let hashes = rlp.list_at(3)?; + + let term = if item_count >= TERM_LEN { + TermMetadata { + last_term_finished_block_num: rlp.val_at(4)?, + current_term_id: rlp.val_at(5)?, + } + } else { + TermMetadata::default() + }; + + let (seq, params) = if item_count >= PARAMS_LEN { + (rlp.val_at(6)?, Some(rlp.val_at(7)?)) + } else { + Default::default() + }; + + let term_params = if item_count >= TERM_PARAMS_LEN { + Some(rlp.val_at(8)?) + } else { + Default::default() + }; + Ok(Self { - number_of_shards: rlp.val_at(1)?, - number_of_initial_shards: rlp.val_at(2)?, - hashes: rlp.list_at(3)?, + number_of_shards, + number_of_initial_shards, + hashes, term, seq, params, + term_params, }) } } @@ -266,6 +297,7 @@ mod tests { term: Default::default(), seq: 0, params: None, + term_params: None, }; let mut rlp = RlpStream::new_list(4); rlp.append(&PREFIX).append(&10u16).append(&1u16).append_list::<H256, H256>(&[]); @@ -281,6 +313,7 @@ mod tests { term: Default::default(), seq: 3, params: Some(CommonParams::default_for_test()), + term_params: Some(CommonParams::default_for_test()), }; rlp_encode_and_decode_test!(metadata); } @@ -297,6 +330,7 @@ mod tests { }, seq: 0, params: None, + term_params: None, }; rlp_encode_and_decode_test!(metadata); } @@ -313,6 +347,24 @@ mod tests { }, seq: 3, params: Some(CommonParams::default_for_test()), + term_params: Some(CommonParams::default_for_test()), + }; + rlp_encode_and_decode_test!(metadata); + } + + #[test] + fn metadata_with_term_and_seq_but_not_term_params() { + let metadata = Metadata { + number_of_shards: 10, + number_of_initial_shards: 1, + hashes: vec![], + term: TermMetadata { + last_term_finished_block_num: 1, + current_term_id: 100, + }, + seq: 3, + params: Some(CommonParams::default_for_test()), + term_params: None, }; rlp_encode_and_decode_test!(metadata); } diff --git a/state/src/traits.rs b/state/src/traits.rs index 40d94688f7..c9414611d1 100644 --- a/state/src/traits.rs +++ b/state/src/traits.rs @@ -183,6 +183,7 @@ pub trait TopState { fn remove_action_data(&mut self, key: &H256); fn update_params(&mut self, metadata_seq: u64, params: CommonParams) -> StateResult<()>; + fn snapshot_term_params(&mut self) -> StateResult<()>; } pub trait StateWithCache { diff --git a/sync/Cargo.toml b/sync/Cargo.toml index c3ad555dc4..caf4846f4f 100644 --- a/sync/Cargo.toml +++ b/sync/Cargo.toml @@ -15,6 +15,7 @@ codechain-network = { path = "../network" } codechain-state = { path = "../state" } codechain-timer = { path = "../util/timer" } codechain-types = { path = "../types" } +hashdb = { path = "../util/hashdb" } journaldb = { path = "../util/journaldb" } kvdb = "0.1" log = "0.4.6" @@ -29,7 +30,6 @@ token-generator = "0.1.0" util-error = { path = "../util/error" } [dev-dependencies] -hashdb = { path = "../util/hashdb" } kvdb-memorydb = "0.1" tempfile = "3.0.4" trie-standardmap = { path = "../util/trie-standardmap" } diff --git a/sync/src/block/downloader/header.rs b/sync/src/block/downloader/header.rs index 963c2135e5..68857da46e 100644 --- a/sync/src/block/downloader/header.rs +++ b/sync/src/block/downloader/header.rs @@ -31,21 +31,14 @@ const MAX_HEADER_QUEUE_LENGTH: usize = 1024; const MAX_RETRY: usize = 3; const MAX_WAIT: u64 = 15; -#[derive(Clone)] -struct Pivot { - hash: BlockHash, - total_score: U256, -} - #[derive(Clone)] pub struct HeaderDownloader { // NOTE: Use this member as minimum as possible. client: Arc<dyn BlockChainClient>, - total_score: U256, + nonce: U256, best_hash: BlockHash, - - pivot: Pivot, + pivot: BlockHash, request_time: Option<Instant>, downloaded: HashMap<BlockHash, Header>, queued: HashMap<BlockHash, Header>, @@ -53,24 +46,15 @@ pub struct HeaderDownloader { } impl HeaderDownloader { - pub fn total_score(&self) -> U256 { - self.total_score - } - - pub fn new(client: Arc<dyn BlockChainClient>, total_score: U256, best_hash: BlockHash) -> Self { + pub fn new(client: Arc<dyn BlockChainClient>, nonce: U256, best_hash: BlockHash) -> Self { let best_header_hash = client.best_block_header().hash(); - let best_score = client.block_total_score(&BlockId::Latest).expect("Best block always exist"); Self { client, - total_score, + nonce, best_hash, - - pivot: Pivot { - hash: best_header_hash, - total_score: best_score, - }, + pivot: best_header_hash, request_time: None, downloaded: HashMap::new(), queued: HashMap::new(), @@ -78,18 +62,23 @@ impl HeaderDownloader { } } - pub fn update(&mut self, total_score: U256, best_hash: BlockHash) -> bool { - match self.total_score.cmp(&total_score) { + pub fn update_pivot(&mut self, hash: BlockHash) { + self.pivot = hash; + } + + pub fn best_hash(&self) -> BlockHash { + self.best_hash + } + + pub fn update(&mut self, nonce: U256, best_hash: BlockHash) -> bool { + match self.nonce.cmp(&nonce) { Ordering::Equal => true, Ordering::Less => { - self.total_score = total_score; + self.nonce = nonce; self.best_hash = best_hash; if self.client.block_header(&BlockId::Hash(best_hash)).is_some() { - self.pivot = Pivot { - hash: best_hash, - total_score, - } + self.pivot = best_hash; } true } @@ -108,25 +97,25 @@ impl HeaderDownloader { /// Find header from queued headers, downloaded cache and then from blockchain /// Panics if header dosn't exist fn pivot_header(&self) -> Header { - match self.queued.get(&self.pivot.hash) { + match self.queued.get(&self.pivot) { Some(header) => header.clone(), - None => match self.downloaded.get(&self.pivot.hash) { + None => match self.downloaded.get(&self.pivot) { Some(header) => header.clone(), - None => self.client.block_header(&BlockId::Hash(self.pivot.hash)).unwrap(), + None => self.client.block_header(&BlockId::Hash(self.pivot)).unwrap(), }, } } - pub fn pivot_score(&self) -> U256 { - self.pivot.total_score - } - pub fn is_idle(&self) -> bool { - let can_request = self.request_time.is_none() && self.total_score > self.pivot.total_score; + let can_request = self.request_time.is_none() && self.best_hash != self.pivot; self.is_valid() && (can_request || self.is_expired()) } + pub fn is_caught_up(&self) -> bool { + self.pivot == self.best_hash + } + pub fn create_request(&mut self) -> Option<RequestMessage> { if !self.is_idle() { return None @@ -154,19 +143,15 @@ impl HeaderDownloader { let pivot_header = self.pivot_header(); // This happens when best_hash is imported by other peer. - if self.best_hash == self.pivot.hash { + if self.best_hash == self.pivot { ctrace!(SYNC, "Ignore received headers, pivot already reached the best hash"); - } else if first_header_hash == self.pivot.hash { + } else if first_header_hash == self.pivot { for header in headers.iter() { self.downloaded.insert(header.hash(), header.clone()); } // FIXME: skip known headers - let new_scores = headers[1..].iter().fold(U256::zero(), |acc, header| acc + header.score()); - self.pivot = Pivot { - hash: headers.last().expect("Last downloaded header must exist").hash(), - total_score: self.pivot.total_score + new_scores, - } + self.pivot = headers.last().expect("Last downloaded header must exist").hash(); } else if first_header_number < pivot_header.number() { ctrace!( SYNC, @@ -174,17 +159,14 @@ impl HeaderDownloader { ); } else if first_header_number == pivot_header.number() { if pivot_header.number() != 0 { - self.pivot = Pivot { - hash: pivot_header.parent_hash(), - total_score: self.pivot.total_score - pivot_header.score(), - } + self.pivot = pivot_header.parent_hash(); } } else { cerror!( SYNC, - "Invalid header update state. best_hash: {}, self.pivot.hash: {}, first_header_hash: {}", + "Invalid header update state. best_hash: {}, self.pivot: {}, first_header_hash: {}", self.best_hash, - self.pivot.hash, + self.pivot, first_header_hash ); } @@ -203,10 +185,7 @@ impl HeaderDownloader { self.downloaded.remove(&hash); if self.best_hash == hash { - self.pivot = Pivot { - hash, - total_score: self.total_score, - } + self.pivot = hash; } } self.queued.shrink_to_fit(); diff --git a/sync/src/block/extension.rs b/sync/src/block/extension.rs index 1ea7eb4c5b..0899560367 100644 --- a/sync/src/block/extension.rs +++ b/sync/src/block/extension.rs @@ -16,20 +16,27 @@ use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; +use std::fs; +use std::mem::discriminant; use std::sync::Arc; use std::time::Duration; use ccore::encoded::Header as EncodedHeader; use ccore::{ - Block, BlockChainClient, BlockChainTrait, BlockId, BlockImportError, ChainNotify, Client, ImportBlock, ImportError, - UnverifiedTransaction, + Block, BlockChainClient, BlockChainTrait, BlockId, BlockImportError, BlockStatus, ChainNotify, Client, ImportBlock, + ImportError, UnverifiedTransaction, }; +use cmerkle::snapshot::ChunkDecompressor; +use cmerkle::snapshot::Restore as SnapshotRestore; +use cmerkle::{skewed_merkle_root, TrieFactory}; use cnetwork::{Api, EventSender, NetworkExtension, NodeId}; use cstate::FindActionHandler; use ctimer::TimerToken; use ctypes::header::{Header, Seal}; use ctypes::transaction::Action; use ctypes::{BlockHash, BlockNumber}; +use hashdb::AsHashDB; +use kvdb::DBTransaction; use primitives::{H256, U256}; use rand::prelude::SliceRandom; use rand::thread_rng; @@ -38,6 +45,7 @@ use token_generator::TokenGenerator; use super::downloader::{BodyDownloader, HeaderDownloader}; use super::message::{Message, RequestMessage, ResponseMessage}; +use crate::snapshot::snapshot_path; const SYNC_TIMER_TOKEN: TimerToken = 0; const SYNC_EXPIRE_TOKEN_BEGIN: TimerToken = SYNC_TIMER_TOKEN + 1; @@ -47,15 +55,28 @@ const SYNC_EXPIRE_TOKEN_END: TimerToken = SYNC_EXPIRE_TOKEN_BEGIN + SYNC_EXPIRE_ const SYNC_TIMER_INTERVAL: u64 = 1000; const SYNC_EXPIRE_REQUEST_INTERVAL: u64 = 15000; -const SNAPSHOT_PERIOD: u64 = (1 << 14); - #[derive(Debug, PartialEq)] pub struct TokenInfo { node_id: NodeId, request_id: Option<u64>, } +#[derive(Debug)] +enum State { + SnapshotHeader(BlockHash, u64), + SnapshotBody { + header: EncodedHeader, + prev_root: H256, + }, + SnapshotChunk { + block: BlockHash, + restore: SnapshotRestore, + }, + Full, +} + pub struct Extension { + state: State, requests: HashMap<NodeId, Vec<(u64, RequestMessage)>>, connected_nodes: HashSet<NodeId>, header_downloaders: HashMap<NodeId, HeaderDownloader>, @@ -66,19 +87,30 @@ pub struct Extension { client: Arc<Client>, api: Box<dyn Api>, last_request: u64, + nonce: u64, + snapshot_dir: Option<String>, } impl Extension { - pub fn new(client: Arc<Client>, api: Box<dyn Api>) -> Extension { + pub fn new( + client: Arc<Client>, + api: Box<dyn Api>, + snapshot_target: Option<(BlockHash, u64)>, + snapshot_dir: Option<String>, + ) -> Extension { api.set_timer(SYNC_TIMER_TOKEN, Duration::from_millis(SYNC_TIMER_INTERVAL)).expect("Timer set succeeds"); + let state = Extension::initial_state(client.clone(), snapshot_target); + cdebug!(SYNC, "Initial state is {:?}", state); let mut header = client.best_header(); let mut hollow_headers = vec![header.decode()]; while client.block_body(&BlockId::Hash(header.hash())).is_none() { - header = client - .block_header(&BlockId::Hash(header.parent_hash())) - .expect("Every imported header must have parent"); - hollow_headers.push(header.decode()); + if let Some(h) = client.block_header(&BlockId::Hash(header.parent_hash())) { + header = h; + hollow_headers.push(header.decode()); + } else { + break + } } let mut body_downloader = BodyDownloader::default(); for neighbors in hollow_headers.windows(2).rev() { @@ -90,6 +122,7 @@ impl Extension { } cinfo!(SYNC, "Sync extension initialized"); Extension { + state, requests: Default::default(), connected_nodes: Default::default(), header_downloaders: Default::default(), @@ -100,6 +133,38 @@ impl Extension { client, api, last_request: Default::default(), + nonce: Default::default(), + snapshot_dir, + } + } + + fn initial_state(client: Arc<Client>, snapshot_target: Option<(BlockHash, u64)>) -> State { + let (hash, num) = match snapshot_target { + Some(target) => target, + None => return State::Full, + }; + let header = match client.block_header(&num.into()) { + Some(ref h) if h.hash() == hash => h.clone(), + _ => return State::SnapshotHeader(hash, num), + }; + if client.block_body(&hash.into()).is_none() { + let parent_hash = header.parent_hash(); + let parent = + client.block_header(&parent_hash.into()).expect("Parent header of the snapshot header must exist"); + return State::SnapshotBody { + header, + prev_root: parent.transactions_root(), + } + } + + let state_db = client.state_db().read(); + let state_root = header.state_root(); + match TrieFactory::readonly(state_db.as_hashdb(), &state_root) { + Ok(ref trie) if trie.is_complete() => State::Full, + _ => State::SnapshotChunk { + block: hash, + restore: SnapshotRestore::new(state_root), + }, } } @@ -109,6 +174,48 @@ impl Extension { } } + fn send_status(&mut self, id: &NodeId) { + if discriminant(&self.state) != discriminant(&State::Full) { + return + } + + let chain_info = self.client.chain_info(); + self.api.send( + id, + Arc::new( + Message::Status { + nonce: U256::from(self.nonce), + best_hash: chain_info.best_proposal_block_hash, + genesis_hash: chain_info.genesis_hash, + } + .rlp_bytes(), + ), + ); + self.nonce += 1; + } + + fn send_status_broadcast(&mut self) { + if discriminant(&self.state) != discriminant(&State::Full) { + return + } + + let chain_info = self.client.chain_info(); + for id in self.connected_nodes.iter() { + self.api.send( + id, + Arc::new( + Message::Status { + nonce: U256::from(self.nonce), + best_hash: chain_info.best_proposal_block_hash, + genesis_hash: chain_info.genesis_hash, + } + .rlp_bytes(), + ), + ); + self.nonce += 1; + } + } + fn send_header_request(&mut self, id: &NodeId, request: RequestMessage) { if let Some(requests) = self.requests.get_mut(id) { ctrace!(SYNC, "Send header request to {}", id); @@ -120,6 +227,14 @@ impl Extension { } fn send_body_request(&mut self, id: &NodeId) { + if let Some(downloader) = self.header_downloaders.get(&id) { + if self.client.block_status(&BlockId::Hash(downloader.best_hash())) == BlockStatus::InChain { + // Peer is lagging behind the local blockchain. + // We don't need to request block bodies to this peer + return + } + } + self.check_sync_variable(); if let Some(requests) = self.requests.get_mut(id) { let have_body_request = { @@ -153,6 +268,37 @@ impl Extension { self.check_sync_variable(); } + fn send_chunk_request(&mut self, block: &BlockHash, root: &H256) { + let have_chunk_request = self.requests.values().flatten().any(|r| match r { + (_, RequestMessage::StateChunk(..)) => true, + _ => false, + }); + + if !have_chunk_request { + let mut peer_ids: Vec<_> = self.header_downloaders.keys().cloned().collect(); + peer_ids.shuffle(&mut thread_rng()); + if let Some(id) = peer_ids.first() { + if let Some(requests) = self.requests.get_mut(&id) { + let req = RequestMessage::StateChunk(*block, vec![*root]); + cdebug!(SYNC, "Request chunk to {} {:?}", id, req); + let request_id = self.last_request; + self.last_request += 1; + requests.push((request_id, req.clone())); + self.api.send(id, Arc::new(Message::Request(request_id, req).rlp_bytes())); + + let token = &self.tokens[id]; + let token_info = self.tokens_info.get_mut(token).unwrap(); + + let _ = self.api.clear_timer(*token); + self.api + .set_timer_once(*token, Duration::from_millis(SYNC_EXPIRE_REQUEST_INTERVAL)) + .expect("Timer set succeeds"); + token_info.request_id = Some(request_id); + } + } + } + } + fn check_sync_variable(&self) { let mut has_error = false; for id in self.header_downloaders.keys() { @@ -164,7 +310,15 @@ impl Extension { let body_requests: Vec<RequestMessage> = requests .iter() .filter_map(|r| match r { - (_, RequestMessage::Bodies(..)) => Some(r.1.clone()), + (_, msg @ RequestMessage::Bodies(..)) => Some(msg.clone()), + _ => None, + }) + .collect(); + + let chunk_requests: Vec<RequestMessage> = requests + .iter() + .filter_map(|r| match r { + (_, msg @ RequestMessage::StateChunk(..)) => Some(msg.clone()), _ => None, }) .collect(); @@ -177,16 +331,18 @@ impl Extension { let token = &self.tokens[id]; let token_info = &self.tokens_info[token]; - match (token_info.request_id, body_requests.len()) { + match (token_info.request_id, body_requests.len() + chunk_requests.len()) { (Some(_), 1) => {} (None, 0) => {} _ => { cerror!( SYNC, - "request_id: {:?}, body_requests.len(): {}, body_requests: {:?}", + "request_id: {:?}, body_requests.len(): {}, body_requests: {:?}, chunk_requests.len(): {}, chunk_requests: {:?}", token_info.request_id, body_requests.len(), - body_requests + body_requests, + chunk_requests.len(), + chunk_requests ); has_error = true; } @@ -212,18 +368,8 @@ impl NetworkExtension<Event> for Extension { fn on_node_added(&mut self, id: &NodeId, _version: u64) { cinfo!(SYNC, "New peer detected #{}", id); - let chain_info = self.client.chain_info(); - self.api.send( - id, - Arc::new( - Message::Status { - total_score: chain_info.best_proposal_score, - best_hash: chain_info.best_proposal_block_hash, - genesis_hash: chain_info.genesis_hash, - } - .rlp_bytes(), - ), - ); + self.send_status(id); + let t = self.connected_nodes.insert(*id); debug_assert!(t, "{} is already added to peer list", id); @@ -272,10 +418,10 @@ impl NetworkExtension<Event> for Extension { if let Ok(received_message) = Rlp::new(data).as_val() { match received_message { Message::Status { - total_score, + nonce, best_hash, genesis_hash, - } => self.on_peer_status(id, total_score, best_hash, genesis_hash), + } => self.on_peer_status(id, nonce, best_hash, genesis_hash), Message::Request(request_id, request) => self.on_peer_request(id, request_id, request), Message::Response(request_id, response) => self.on_peer_response(id, request_id, response), } @@ -287,27 +433,66 @@ impl NetworkExtension<Event> for Extension { fn on_timeout(&mut self, token: TimerToken) { match token { SYNC_TIMER_TOKEN => { - let best_proposal_score = self.client.chain_info().best_proposal_score; let mut peer_ids: Vec<_> = self.header_downloaders.keys().cloned().collect(); peer_ids.shuffle(&mut thread_rng()); - for id in &peer_ids { - let request = self.header_downloaders.get_mut(id).and_then(HeaderDownloader::create_request); - if let Some(request) = request { - self.send_header_request(id, request); - break + match self.state { + State::SnapshotHeader(_, num) => { + for id in &peer_ids { + self.send_header_request(id, RequestMessage::Headers { + start_number: num - 1, + max_count: 2, + }); + } } - } - - for id in peer_ids { - let peer_score = if let Some(peer) = self.header_downloaders.get(&id) { - peer.total_score() - } else { - U256::zero() - }; + State::SnapshotBody { + ref header, + .. + } => { + for id in &peer_ids { + if let Some(requests) = self.requests.get_mut(id) { + ctrace!(SYNC, "Send snapshot body request to {}", id); + let request = RequestMessage::Bodies(vec![header.hash()]); + let request_id = self.last_request; + self.last_request += 1; + requests.push((request_id, request.clone())); + self.api.send(id, Arc::new(Message::Request(request_id, request).rlp_bytes())); + + let token = &self.tokens[id]; + let token_info = self.tokens_info.get_mut(token).unwrap(); + + let _ = self.api.clear_timer(*token); + self.api + .set_timer_once(*token, Duration::from_millis(SYNC_EXPIRE_REQUEST_INTERVAL)) + .expect("Timer set succeeds"); + token_info.request_id = Some(request_id); + } + } + } + State::SnapshotChunk { + block, + ref mut restore, + } => { + if let Some(root) = restore.next_to_feed() { + self.send_chunk_request(&block, &root); + } else { + self.client.force_update_best_block(&block); + self.transition_to_full(); + } + } + State::Full => { + for id in &peer_ids { + let request = + self.header_downloaders.get_mut(id).and_then(HeaderDownloader::create_request); + if let Some(request) = request { + self.send_header_request(id, request); + break + } + } - if peer_score > best_proposal_score { - self.send_body_request(&id); + for id in peer_ids { + self.send_body_request(&id); + } } } } @@ -386,61 +571,44 @@ pub enum Event { impl Extension { fn new_headers(&mut self, imported: Vec<BlockHash>, enacted: Vec<BlockHash>, retracted: Vec<BlockHash>) { - let peer_ids: Vec<_> = self.header_downloaders.keys().cloned().collect(); - for id in peer_ids { - if let Some(peer) = self.header_downloaders.get_mut(&id) { + if let State::Full = self.state { + for peer in self.header_downloaders.values_mut() { peer.mark_as_imported(imported.clone()); } + let mut headers_to_download: Vec<_> = enacted + .into_iter() + .map(|hash| self.client.block_header(&BlockId::Hash(hash)).expect("Enacted header must exist")) + .collect(); + headers_to_download.sort_unstable_by_key(EncodedHeader::number); + #[allow(clippy::redundant_closure)] + // False alarm. https://github.com/rust-lang/rust-clippy/issues/1439 + headers_to_download.dedup_by_key(|h| h.hash()); + + let headers: Vec<_> = headers_to_download + .into_iter() + .filter(|header| self.client.block_body(&BlockId::Hash(header.hash())).is_none()) + .collect(); // FIXME: No need to collect here if self is not borrowed. + for header in headers { + let parent = self + .client + .block_header(&BlockId::Hash(header.parent_hash())) + .expect("Enacted header must have parent"); + let is_empty = header.transactions_root() == parent.transactions_root(); + self.body_downloader.add_target(&header.decode(), is_empty); + } + self.body_downloader.remove_target(&retracted); } - let mut headers_to_download: Vec<_> = enacted - .into_iter() - .map(|hash| self.client.block_header(&BlockId::Hash(hash)).expect("Enacted header must exist")) - .collect(); - headers_to_download.sort_unstable_by_key(EncodedHeader::number); - #[allow(clippy::redundant_closure)] - // False alarm. https://github.com/rust-lang/rust-clippy/issues/1439 - headers_to_download.dedup_by_key(|h| h.hash()); - - let headers: Vec<_> = headers_to_download - .into_iter() - .filter(|header| self.client.block_body(&BlockId::Hash(header.hash())).is_none()) - .collect(); // FIXME: No need to collect here if self is not borrowed. - for header in headers { - let parent = self - .client - .block_header(&BlockId::Hash(header.parent_hash())) - .expect("Enacted header must have parent"); - let is_empty = header.transactions_root() == parent.transactions_root(); - self.body_downloader.add_target(&header.decode(), is_empty); - } - self.body_downloader.remove_target(&retracted); } fn new_blocks(&mut self, imported: Vec<BlockHash>, invalid: Vec<BlockHash>) { self.body_downloader.remove_target(&imported); self.body_downloader.remove_target(&invalid); - - - let chain_info = self.client.chain_info(); - - for id in &self.connected_nodes { - self.api.send( - id, - Arc::new( - Message::Status { - total_score: chain_info.best_proposal_score, - best_hash: chain_info.best_proposal_block_hash, - genesis_hash: chain_info.genesis_hash, - } - .rlp_bytes(), - ), - ); - } + self.send_status_broadcast(); } } impl Extension { - fn on_peer_status(&mut self, from: &NodeId, total_score: U256, best_hash: BlockHash, genesis_hash: BlockHash) { + fn on_peer_status(&mut self, from: &NodeId, nonce: U256, best_hash: BlockHash, genesis_hash: BlockHash) { // Validity check if genesis_hash != self.client.chain_info().genesis_hash { cinfo!(SYNC, "Genesis hash mismatch with peer {}", from); @@ -449,21 +617,21 @@ impl Extension { match self.header_downloaders.entry(*from) { Entry::Occupied(mut peer) => { - if !peer.get_mut().update(total_score, best_hash) { + if !peer.get_mut().update(nonce, best_hash) { // FIXME: It should be an error level if the consensus is PoW. cdebug!(SYNC, "Peer #{} status updated but score is less than before", from); return } } Entry::Vacant(e) => { - e.insert(HeaderDownloader::new(self.client.clone(), total_score, best_hash)); + e.insert(HeaderDownloader::new(self.client.clone(), nonce, best_hash)); } } - cinfo!(SYNC, "Peer #{} status update: total_score: {}, best_hash: {}", from, total_score, best_hash); + cinfo!(SYNC, "Peer #{} status update: nonce: {}, best_hash: {}", from, nonce, best_hash); } fn on_peer_request(&self, from: &NodeId, id: u64, request: RequestMessage) { - if !self.header_downloaders.contains_key(from) { + if !self.connected_nodes.contains(from) { cinfo!(SYNC, "Request from invalid peer #{} received", from); return } @@ -485,11 +653,9 @@ impl Extension { ctrace!(SYNC, "Received body request from {}", from); self.create_bodies_response(hashes) } - RequestMessage::StateHead(hash) => self.create_state_head_response(hash), - RequestMessage::StateChunk { - block_hash, - tree_root, - } => self.create_state_chunk_response(block_hash, tree_root), + RequestMessage::StateChunk(block_hash, chunk_root) => { + self.create_state_chunk_response(block_hash, chunk_root) + } }; self.api.send(from, Arc::new(Message::Response(id, response).rlp_bytes())); @@ -501,21 +667,9 @@ impl Extension { .. } => true, RequestMessage::Bodies(hashes) => !hashes.is_empty(), - RequestMessage::StateHead(hash) => match self.client.block_number(&BlockId::Hash(*hash)) { - Some(number) if number % SNAPSHOT_PERIOD == 0 => true, - _ => false, - }, RequestMessage::StateChunk { - block_hash, .. - } => { - let _is_checkpoint = match self.client.block_number(&BlockId::Hash(*block_hash)) { - Some(number) if number % SNAPSHOT_PERIOD == 0 => true, - _ => false, - }; - // FIXME: check tree_root - unimplemented!() - } + } => true, } } @@ -553,12 +707,18 @@ impl Extension { ResponseMessage::Bodies(bodies) } - fn create_state_head_response(&self, _hash: BlockHash) -> ResponseMessage { - unimplemented!() - } - - fn create_state_chunk_response(&self, _hash: BlockHash, _tree_root: H256) -> ResponseMessage { - unimplemented!() + fn create_state_chunk_response(&self, hash: BlockHash, chunk_roots: Vec<H256>) -> ResponseMessage { + let mut result = Vec::new(); + for root in chunk_roots { + if let Some(dir) = &self.snapshot_dir { + let chunk_path = snapshot_path(&dir, &hash, &root); + match fs::read(chunk_path) { + Ok(chunk) => result.push(chunk), + _ => result.push(Vec::new()), + } + } + } + ResponseMessage::StateChunk(result) } fn on_peer_response(&mut self, from: &NodeId, id: u64, mut response: ResponseMessage) { @@ -598,7 +758,24 @@ impl Extension { self.on_body_response(hashes, bodies); self.check_sync_variable(); } - _ => unimplemented!(), + ResponseMessage::StateChunk(chunks) => { + let roots = match request { + RequestMessage::StateChunk(_, roots) => roots, + _ => unreachable!(), + }; + if let Some(token) = self.tokens.get(from) { + if let Some(token_info) = self.tokens_info.get_mut(token) { + if token_info.request_id.is_none() { + ctrace!(SYNC, "Expired before handling response"); + return + } + self.api.clear_timer(*token).expect("Timer clear succeed"); + token_info.request_id = None; + } + } + self.dismiss_request(from, id); + self.on_chunk_response(from, &roots, &chunks); + } } } } @@ -652,13 +829,10 @@ impl Extension { } true } - (RequestMessage::StateHead(..), ResponseMessage::StateHead(..)) => unimplemented!(), - ( - RequestMessage::StateChunk { - .. - }, - ResponseMessage::StateChunk(..), - ) => unimplemented!(), + (RequestMessage::StateChunk(_, roots), ResponseMessage::StateChunk(chunks)) => { + // Check length + roots.len() == chunks.len() + } _ => { cwarn!(SYNC, "Invalid response type"); false @@ -668,95 +842,228 @@ impl Extension { fn on_header_response(&mut self, from: &NodeId, headers: &[Header]) { ctrace!(SYNC, "Received header response from({}) with length({})", from, headers.len()); - let (mut completed, pivot_score_changed) = if let Some(peer) = self.header_downloaders.get_mut(from) { - let before_pivot_score = peer.pivot_score(); - let encoded: Vec<_> = headers.iter().map(|h| EncodedHeader::new(h.rlp_bytes().to_vec())).collect(); - peer.import_headers(&encoded); - let after_pivot_score = peer.pivot_score(); - (peer.downloaded(), before_pivot_score != after_pivot_score) - } else { - (Vec::new(), false) - }; - completed.sort_unstable_by_key(EncodedHeader::number); - - let mut exists = Vec::new(); - let mut queued = Vec::new(); - - for header in completed { - let hash = header.hash(); - match self.client.import_header(header.clone().into_inner()) { - Err(BlockImportError::Import(ImportError::AlreadyInChain)) => exists.push(hash), - Err(BlockImportError::Import(ImportError::AlreadyQueued)) => queued.push(hash), - // FIXME: handle import errors - Err(err) => { - cwarn!(SYNC, "Cannot import header({}): {:?}", header.hash(), err); - break + match self.state { + State::SnapshotHeader(hash, _) => match headers { + [parent, header] if header.hash() == hash => { + match self.client.import_trusted_header(parent) { + Ok(_) + | Err(BlockImportError::Import(ImportError::AlreadyInChain)) + | Err(BlockImportError::Import(ImportError::AlreadyQueued)) => {} + Err(err) => { + cwarn!(SYNC, "Cannot import header({}): {:?}", parent.hash(), err); + return + } + } + match self.client.import_trusted_header(header) { + Ok(_) + | Err(BlockImportError::Import(ImportError::AlreadyInChain)) + | Err(BlockImportError::Import(ImportError::AlreadyQueued)) => {} + Err(err) => { + cwarn!(SYNC, "Cannot import header({}): {:?}", header.hash(), err); + return + } + } + self.state = State::SnapshotBody { + header: EncodedHeader::new(header.rlp_bytes().to_vec()), + prev_root: *parent.transactions_root(), + }; + cdebug!(SYNC, "Transitioning state to {:?}", self.state); } - _ => {} - } - } + _ => cdebug!( + SYNC, + "Peer {} responded with a invalid response. requested hash: {}, response length: {}", + from, + hash, + headers.len() + ), + }, + State::SnapshotBody { + .. + } => {} + State::SnapshotChunk { + .. + } => {} + State::Full => { + let (mut completed, peer_is_caught_up) = if let Some(peer) = self.header_downloaders.get_mut(from) { + let encoded: Vec<_> = headers.iter().map(|h| EncodedHeader::new(h.rlp_bytes().to_vec())).collect(); + peer.import_headers(&encoded); + (peer.downloaded(), peer.is_caught_up()) + } else { + (Vec::new(), true) + }; + completed.sort_unstable_by_key(EncodedHeader::number); - let request = self.header_downloaders.get_mut(from).and_then(|peer| { - peer.mark_as_queued(queued); - peer.mark_as_imported(exists); - peer.create_request() - }); - if pivot_score_changed { - if let Some(request) = request { - self.send_header_request(from, request); + let mut exists = Vec::new(); + let mut queued = Vec::new(); + + for header in completed { + let hash = header.hash(); + match self.client.import_header(header.clone().into_inner()) { + Err(BlockImportError::Import(ImportError::AlreadyInChain)) => exists.push(hash), + Err(BlockImportError::Import(ImportError::AlreadyQueued)) => queued.push(hash), + // FIXME: handle import errors + Err(err) => { + cwarn!(SYNC, "Cannot import header({}): {:?}", header.hash(), err); + break + } + _ => {} + } + } + + let request = self.header_downloaders.get_mut(from).and_then(|peer| { + peer.mark_as_queued(queued); + peer.mark_as_imported(exists); + peer.create_request() + }); + if !peer_is_caught_up { + if let Some(request) = request { + self.send_header_request(from, request); + } + } } } } fn on_body_response(&mut self, hashes: Vec<BlockHash>, bodies: Vec<Vec<UnverifiedTransaction>>) { ctrace!(SYNC, "Received body response with lenth({}) {:?}", hashes.len(), hashes); - { - self.body_downloader.import_bodies(hashes, bodies); - let completed = self.body_downloader.drain(); - for (hash, transactions) in completed { - let header = self - .client - .block_header(&BlockId::Hash(hash)) - .expect("Downloaded body's header must exist") - .decode(); - let block = Block { - header, - transactions, - }; - cdebug!(SYNC, "Body download completed for #{}({})", block.header.number(), hash); - match self.client.import_block(block.rlp_bytes(&Seal::With)) { - Err(BlockImportError::Import(ImportError::AlreadyInChain)) => { - cwarn!(SYNC, "Downloaded already existing block({})", hash) - } - Err(BlockImportError::Import(ImportError::AlreadyQueued)) => { - cwarn!(SYNC, "Downloaded already queued in the verification queue({})", hash) - } - Err(err) => { + + match self.state { + State::SnapshotBody { + ref header, + prev_root, + } => { + let body = bodies.first().expect("Body response in SnapshotBody state has only one body"); + let new_root = skewed_merkle_root(prev_root, body.iter().map(Encodable::rlp_bytes)); + if header.transactions_root() == new_root { + let block = Block { + header: header.decode(), + transactions: body.clone(), + }; + match self.client.import_trusted_block(&block) { + Ok(_) | Err(BlockImportError::Import(ImportError::AlreadyInChain)) => { + self.state = State::SnapshotChunk { + block: header.hash(), + restore: SnapshotRestore::new(header.state_root()), + }; + cdebug!(SYNC, "Transitioning state to {:?}", self.state); + } + Err(BlockImportError::Import(ImportError::AlreadyQueued)) => {} // FIXME: handle import errors - cwarn!(SYNC, "Cannot import block({}): {:?}", hash, err); - break + Err(err) => { + cwarn!(SYNC, "Cannot import block({}): {:?}", header.hash(), err); + } } - _ => {} } } + State::Full => { + { + self.body_downloader.import_bodies(hashes, bodies); + let completed = self.body_downloader.drain(); + for (hash, transactions) in completed { + let header = self + .client + .block_header(&BlockId::Hash(hash)) + .expect("Downloaded body's header must exist") + .decode(); + let block = Block { + header, + transactions, + }; + cdebug!(SYNC, "Body download completed for #{}({})", block.header.number(), hash); + match self.client.import_block(block.rlp_bytes(&Seal::With)) { + Err(BlockImportError::Import(ImportError::AlreadyInChain)) => { + cwarn!(SYNC, "Downloaded already existing block({})", hash) + } + Err(BlockImportError::Import(ImportError::AlreadyQueued)) => { + cwarn!(SYNC, "Downloaded already queued in the verification queue({})", hash) + } + Err(err) => { + // FIXME: handle import errors + cwarn!(SYNC, "Cannot import block({}): {:?}", hash, err); + break + } + _ => {} + } + } + } + + let mut peer_ids: Vec<_> = self.header_downloaders.keys().cloned().collect(); + peer_ids.shuffle(&mut thread_rng()); + + for id in peer_ids { + self.send_body_request(&id); + } + } + _ => {} } + } + + fn on_chunk_response(&mut self, from: &NodeId, roots: &[H256], chunks: &[Vec<u8>]) { + if let State::SnapshotChunk { + block, + ref mut restore, + } = self.state + { + for (r, c) in roots.iter().zip(chunks) { + if c.is_empty() { + cdebug!(SYNC, "Peer {} sent empty response for chunk request {}", from, r); + continue + } + let decompressor = ChunkDecompressor::from_slice(c); + let raw_chunk = match decompressor.decompress() { + Ok(chunk) => chunk, + Err(e) => { + cwarn!(SYNC, "Decode failed for chunk response from peer {}: {}", from, e); + continue + } + }; + let recovered = match raw_chunk.recover(*r) { + Ok(chunk) => chunk, + Err(e) => { + cwarn!(SYNC, "Invalid chunk response from peer {}: {}", from, e); + continue + } + }; - let total_score = self.client.chain_info().best_proposal_score; - let mut peer_ids: Vec<_> = self.header_downloaders.keys().cloned().collect(); - peer_ids.shuffle(&mut thread_rng()); + let batch = { + let mut state_db = self.client.state_db().write(); + let hash_db = state_db.as_hashdb_mut(); + restore.feed(hash_db, recovered); + + let mut batch = DBTransaction::new(); + match state_db.journal_under(&mut batch, 0, H256::zero()) { + Ok(_) => batch, + Err(e) => { + cwarn!(SYNC, "Failed to write state chunk to database: {}", e); + continue + } + } + }; + self.client.db().write_buffered(batch); + match self.client.db().flush() { + Ok(_) => cdebug!(SYNC, "Wrote state chunk to database: {}", r), + Err(e) => cwarn!(SYNC, "Failed to flush database: {}", e), + } + } - for id in peer_ids { - let peer_score = if let Some(peer) = self.header_downloaders.get(&id) { - peer.total_score() + if let Some(root) = restore.next_to_feed() { + self.send_chunk_request(&block, &root); } else { - U256::zero() - }; - - if peer_score > total_score { - self.send_body_request(&id); + self.client.force_update_best_block(&block); + self.transition_to_full(); } } } + + fn transition_to_full(&mut self) { + cdebug!(SYNC, "Transitioning state to {:?}", State::Full); + let best_hash = self.client.best_block_header().hash(); + for downloader in self.header_downloaders.values_mut() { + downloader.update_pivot(best_hash); + } + self.state = State::Full; + self.send_status_broadcast(); + } } pub struct BlockSyncSender(EventSender<Event>); diff --git a/sync/src/block/message/mod.rs b/sync/src/block/message/mod.rs index 42b9654981..04b3236f2a 100644 --- a/sync/src/block/message/mod.rs +++ b/sync/src/block/message/mod.rs @@ -29,15 +29,13 @@ const MESSAGE_ID_GET_HEADERS: u8 = 0x02; const MESSAGE_ID_HEADERS: u8 = 0x03; const MESSAGE_ID_GET_BODIES: u8 = 0x04; const MESSAGE_ID_BODIES: u8 = 0x05; -const MESSAGE_ID_GET_STATE_HEAD: u8 = 0x06; -const MESSAGE_ID_STATE_HEAD: u8 = 0x07; -const MESSAGE_ID_GET_STATE_CHUNK: u8 = 0x08; -const MESSAGE_ID_STATE_CHUNK: u8 = 0x09; +const MESSAGE_ID_GET_STATE_CHUNK: u8 = 0x0a; +const MESSAGE_ID_STATE_CHUNK: u8 = 0x0b; #[derive(Debug, PartialEq)] pub enum Message { Status { - total_score: U256, + nonce: U256, best_hash: BlockHash, genesis_hash: BlockHash, }, @@ -49,7 +47,7 @@ impl Encodable for Message { fn rlp_append(&self, s: &mut RlpStream) { match self { Message::Status { - total_score, + nonce, best_hash, genesis_hash, } => { @@ -57,7 +55,7 @@ impl Encodable for Message { s.append(&MESSAGE_ID_STATUS); s.begin_list(3); - s.append(total_score); + s.append(nonce); s.append(best_hash); s.append(genesis_hash); } @@ -99,7 +97,7 @@ impl Decodable for Message { } Ok(Message::Status { - total_score: message.val_at(0)?, + nonce: message.val_at(0)?, best_hash: message.val_at(1)?, genesis_hash: message.val_at(2)?, }) @@ -114,11 +112,10 @@ impl Decodable for Message { let request_id = rlp.val_at(1)?; let message = rlp.at(2)?; match id { - MESSAGE_ID_GET_HEADERS - | MESSAGE_ID_GET_BODIES - | MESSAGE_ID_GET_STATE_HEAD - | MESSAGE_ID_GET_STATE_CHUNK => Ok(Message::Request(request_id, RequestMessage::decode(id, &message)?)), - MESSAGE_ID_HEADERS | MESSAGE_ID_BODIES | MESSAGE_ID_STATE_HEAD | MESSAGE_ID_STATE_CHUNK => { + MESSAGE_ID_GET_HEADERS | MESSAGE_ID_GET_BODIES | MESSAGE_ID_GET_STATE_CHUNK => { + Ok(Message::Request(request_id, RequestMessage::decode(id, &message)?)) + } + MESSAGE_ID_HEADERS | MESSAGE_ID_BODIES | MESSAGE_ID_STATE_CHUNK => { Ok(Message::Response(request_id, ResponseMessage::decode(id, &message)?)) } _ => Err(DecoderError::Custom("Unknown message id detected")), @@ -137,7 +134,7 @@ mod tests { #[test] fn status_message_rlp() { rlp_encode_and_decode_test!(Message::Status { - total_score: U256::default(), + nonce: U256::zero(), best_hash: H256::default().into(), genesis_hash: H256::default().into(), }); @@ -148,10 +145,4 @@ mod tests { let request_id = 10; rlp_encode_and_decode_test!(Message::Request(request_id, RequestMessage::Bodies(vec![]))); } - - #[test] - fn request_state_head_rlp() { - let request_id = 10; - rlp_encode_and_decode_test!(Message::Request(request_id, RequestMessage::StateHead(H256::random().into()))); - } } diff --git a/sync/src/block/message/request.rs b/sync/src/block/message/request.rs index 6c7ef53010..ddc11ecf85 100644 --- a/sync/src/block/message/request.rs +++ b/sync/src/block/message/request.rs @@ -25,11 +25,7 @@ pub enum RequestMessage { max_count: u64, }, Bodies(Vec<BlockHash>), - StateHead(BlockHash), - StateChunk { - block_hash: BlockHash, - tree_root: H256, - }, + StateChunk(BlockHash, Vec<H256>), } impl Encodable for RequestMessage { @@ -46,17 +42,10 @@ impl Encodable for RequestMessage { RequestMessage::Bodies(hashes) => { s.append_list(hashes); } - RequestMessage::StateHead(block_hash) => { - s.begin_list(1); - s.append(block_hash); - } - RequestMessage::StateChunk { - block_hash, - tree_root, - } => { + RequestMessage::StateChunk(block_hash, merkle_roots) => { s.begin_list(2); s.append(block_hash); - s.append(tree_root); + s.append_list(merkle_roots); } }; } @@ -69,7 +58,6 @@ impl RequestMessage { .. } => super::MESSAGE_ID_GET_HEADERS, RequestMessage::Bodies(..) => super::MESSAGE_ID_GET_BODIES, - RequestMessage::StateHead(..) => super::MESSAGE_ID_GET_STATE_HEAD, RequestMessage::StateChunk { .. } => super::MESSAGE_ID_GET_STATE_CHUNK, @@ -92,16 +80,6 @@ impl RequestMessage { } } super::MESSAGE_ID_GET_BODIES => RequestMessage::Bodies(rlp.as_list()?), - super::MESSAGE_ID_GET_STATE_HEAD => { - let item_count = rlp.item_count()?; - if item_count != 1 { - return Err(DecoderError::RlpIncorrectListLen { - got: item_count, - expected: 1, - }) - } - RequestMessage::StateHead(rlp.val_at(0)?) - } super::MESSAGE_ID_GET_STATE_CHUNK => { let item_count = rlp.item_count()?; if item_count != 2 { @@ -110,10 +88,7 @@ impl RequestMessage { expected: 2, }) } - RequestMessage::StateChunk { - block_hash: rlp.val_at(0)?, - tree_root: rlp.val_at(1)?, - } + RequestMessage::StateChunk(rlp.val_at(0)?, rlp.list_at(1)?) } _ => return Err(DecoderError::Custom("Unknown message id detected")), }; @@ -149,18 +124,9 @@ mod tests { assert_eq!(message, decode_bytes(message.message_id(), message.rlp_bytes().as_ref())); } - #[test] - fn request_state_head_message_rlp() { - let message = RequestMessage::StateHead(H256::default().into()); - assert_eq!(message, decode_bytes(message.message_id(), message.rlp_bytes().as_ref())); - } - #[test] fn request_state_chunk_message_rlp() { - let message = RequestMessage::StateChunk { - block_hash: H256::default().into(), - tree_root: H256::default(), - }; + let message = RequestMessage::StateChunk(H256::default().into(), vec![H256::default()]); assert_eq!(message, decode_bytes(message.message_id(), message.rlp_bytes().as_ref())); } } diff --git a/sync/src/block/message/response.rs b/sync/src/block/message/response.rs index f6a16f2f97..823f26d490 100644 --- a/sync/src/block/message/response.rs +++ b/sync/src/block/message/response.rs @@ -24,8 +24,7 @@ use ctypes::Header; pub enum ResponseMessage { Headers(Vec<Header>), Bodies(Vec<Vec<UnverifiedTransaction>>), - StateHead(Vec<u8>), - StateChunk(Vec<u8>), + StateChunk(Vec<Vec<u8>>), } impl Encodable for ResponseMessage { @@ -53,13 +52,8 @@ impl Encodable for ResponseMessage { s.append(&compressed); } - ResponseMessage::StateHead(bytes) => { - s.begin_list(1); - s.append(bytes); - } - ResponseMessage::StateChunk(bytes) => { - s.begin_list(1); - s.append(bytes); + ResponseMessage::StateChunk(chunks) => { + s.append_list::<Vec<u8>, Vec<u8>>(chunks); } }; } @@ -72,7 +66,6 @@ impl ResponseMessage { .. } => super::MESSAGE_ID_HEADERS, ResponseMessage::Bodies(..) => super::MESSAGE_ID_BODIES, - ResponseMessage::StateHead(..) => super::MESSAGE_ID_STATE_HEAD, ResponseMessage::StateChunk { .. } => super::MESSAGE_ID_STATE_CHUNK, @@ -109,26 +102,7 @@ impl ResponseMessage { } ResponseMessage::Bodies(bodies) } - super::MESSAGE_ID_STATE_HEAD => { - let item_count = rlp.item_count()?; - if item_count != 1 { - return Err(DecoderError::RlpIncorrectListLen { - got: item_count, - expected: 1, - }) - } - ResponseMessage::StateHead(rlp.val_at(0)?) - } - super::MESSAGE_ID_STATE_CHUNK => { - let item_count = rlp.item_count()?; - if item_count != 1 { - return Err(DecoderError::RlpIncorrectListLen { - got: item_count, - expected: 1, - }) - } - ResponseMessage::StateChunk(rlp.val_at(0)?) - } + super::MESSAGE_ID_STATE_CHUNK => ResponseMessage::StateChunk(rlp.as_list()?), _ => return Err(DecoderError::Custom("Unknown message id detected")), }; @@ -184,12 +158,6 @@ mod tests { assert_eq!(message, decode_bytes(message.message_id(), message.rlp_bytes().as_ref())); } - #[test] - fn state_head_message_rlp() { - let message = ResponseMessage::StateHead(vec![]); - assert_eq!(message, decode_bytes(message.message_id(), message.rlp_bytes().as_ref())); - } - #[test] fn state_chunk_message_rlp() { let message = ResponseMessage::StateChunk(vec![]); diff --git a/sync/src/lib.rs b/sync/src/lib.rs index ff2fc0ab84..5a0b1c5910 100644 --- a/sync/src/lib.rs +++ b/sync/src/lib.rs @@ -25,7 +25,6 @@ extern crate codechain_state as cstate; extern crate codechain_timer as ctimer; extern crate codechain_types as ctypes; -#[cfg(test)] extern crate hashdb; extern crate journaldb; extern crate kvdb; @@ -47,11 +46,10 @@ extern crate trie_standardmap; extern crate util_error; mod block; -mod snapshot; +pub mod snapshot; mod transaction; pub use crate::block::{BlockSyncEvent, BlockSyncExtension, BlockSyncSender}; -pub use crate::snapshot::SnapshotService; pub use crate::transaction::TransactionSyncExtension; #[cfg(test)] diff --git a/sync/src/snapshot/error.rs b/sync/src/snapshot/error.rs deleted file mode 100644 index dba9adc024..0000000000 --- a/sync/src/snapshot/error.rs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2018 Kodebox, Inc. -// This file is part of CodeChain. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -use std::fmt::{Display, Formatter, Result as FormatResult}; -use std::io::{Error as FileError, ErrorKind}; - -use primitives::H256; -use util_error::UtilError; - -#[derive(Debug)] -pub enum Error { - NodeNotFound(H256), - SyncError(String), - FileError(ErrorKind), - UtilError(UtilError), -} - -impl From<FileError> for Error { - fn from(error: FileError) -> Self { - Error::FileError(error.kind()) - } -} - -impl From<UtilError> for Error { - fn from(error: UtilError) -> Self { - Error::UtilError(error) - } -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> FormatResult { - match self { - Error::NodeNotFound(key) => write!(f, "State node not found: {:x}", key), - Error::SyncError(reason) => write!(f, "Sync error: {}", reason), - Error::FileError(kind) => write!(f, "File system error: {:?}", kind), - Error::UtilError(error) => write!(f, "Util error: {:?}", error), - } - } -} diff --git a/sync/src/snapshot/mod.rs b/sync/src/snapshot/mod.rs index f0e8c6bedc..7a075284a2 100644 --- a/sync/src/snapshot/mod.rs +++ b/sync/src/snapshot/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2018 Kodebox, Inc. +// Copyright 2019 Kodebox, Inc. // This file is part of CodeChain. // // This program is free software: you can redistribute it and/or modify @@ -14,9 +14,173 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -mod error; -mod service; -#[cfg_attr(feature = "cargo-clippy", allow(clippy::module_inception))] -mod snapshot; -pub use self::service::Service as SnapshotService; +use std::fs; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; +use std::thread::{spawn, JoinHandle}; + +use ccore::snapshot_notify::{NotifyReceiverSource, ReceiverCanceller}; +use ccore::{BlockChainClient, BlockChainTrait, BlockId, Client}; +use cmerkle::snapshot::{ChunkCompressor, Error as SnapshotError, Snapshot}; +use ctypes::BlockHash; +use hashdb::{AsHashDB, HashDB}; +use primitives::H256; + +pub struct Service { + join_handle: Option<JoinHandle<()>>, + canceller: Option<ReceiverCanceller>, +} + +pub fn snapshot_dir(root_dir: &str, block: &BlockHash) -> PathBuf { + let mut path = PathBuf::new(); + path.push(root_dir); + path.push(format!("{:x}", **block)); + path +} + +pub fn snapshot_path(root_dir: &str, block: &BlockHash, chunk_root: &H256) -> PathBuf { + let mut path = snapshot_dir(root_dir, block); + path.push(format!("{:x}", chunk_root)); + path +} + +impl Service { + pub fn new( + client: Arc<Client>, + notify_receiver_source: NotifyReceiverSource, + root_dir: String, + expiration: Option<u64>, + ) -> Self { + let NotifyReceiverSource(canceller, receiver) = notify_receiver_source; + let join_handle = spawn(move || { + cinfo!(SYNC, "Snapshot service is on"); + while let Ok(block_hash) = receiver.recv() { + cinfo!(SYNC, "Snapshot is requested for block: {}", block_hash); + let state_root = if let Some(header) = client.block_header(&BlockId::Hash(block_hash)) { + header.state_root() + } else { + cerror!(SYNC, "There isn't corresponding header for the requested block hash: {}", block_hash,); + continue + }; + { + let db_lock = client.state_db().read(); + if let Err(err) = snapshot(db_lock.as_hashdb(), block_hash, state_root, &root_dir) { + cerror!( + SYNC, + "Snapshot request failed for block: {}, chunk_root: {}, err: {}", + block_hash, + state_root, + err + ); + } else { + cinfo!(SYNC, "Snapshot is ready for block: {}", block_hash) + } + } + + if let Some(expiration) = expiration { + if let Err(err) = cleanup_expired(&client, &root_dir, expiration) { + cerror!(SYNC, "Snapshot cleanup error after block hash {}, err: {}", block_hash, err); + } + } + } + cinfo!(SYNC, "Snapshot service is stopped") + }); + + Self { + canceller: Some(canceller), + join_handle: Some(join_handle), + } + } +} + +fn snapshot(db: &dyn HashDB, block_hash: BlockHash, chunk_root: H256, root_dir: &str) -> Result<(), SnapshotError> { + let snapshot_dir = snapshot_dir(root_dir, &block_hash); + fs::create_dir_all(snapshot_dir)?; + + for chunk in Snapshot::from_hashdb(db, chunk_root) { + let chunk_path = snapshot_path(root_dir, &block_hash, &chunk.root); + let chunk_file = fs::File::create(chunk_path)?; + let compressor = ChunkCompressor::new(chunk_file); + compressor.compress_chunk(&chunk)?; + } + + Ok(()) +} + +fn cleanup_expired(client: &Client, root_dir: &str, expiration: u64) -> Result<(), SnapshotError> { + for entry in fs::read_dir(root_dir)? { + let entry = match entry { + Ok(entry) => entry, + Err(err) => { + cerror!(SYNC, "Snapshot cleanup can't retrieve entry. err: {}", err); + continue + } + }; + let path = entry.path(); + + match entry.file_type().map(|x| x.is_dir()) { + Ok(true) => {} + Ok(false) => continue, + Err(err) => { + cerror!(SYNC, "Snapshot cleanup can't retrieve file info: {}, err: {}", path.to_string_lossy(), err); + continue + } + } + + let name = match path.file_name().expect("Directories always have file name").to_str() { + Some(n) => n, + None => continue, + }; + let hash = match H256::from_str(name) { + Ok(h) => BlockHash::from(h), + Err(_) => continue, + }; + let number = if let Some(number) = client.block_number(&BlockId::Hash(hash)) { + number + } else { + cerror!(SYNC, "Snapshot cleanup can't retrieve block number for block_hash: {}", hash); + continue + }; + + if number + expiration < client.best_block_header().number() { + cleanup_snapshot(root_dir, hash) + } + } + Ok(()) +} + +/// Remove all files in `root_dir/block_hash` +fn cleanup_snapshot(root_dir: &str, block_hash: BlockHash) { + let path = snapshot_dir(root_dir, &block_hash); + let rename_to = PathBuf::from(root_dir).join(format!("{:x}.old", *block_hash)); + // It is okay to ignore errors. We just wanted them to be removed. + match fs::rename(path, &rename_to) { + Ok(()) => {} + Err(err) => { + cerror!(SYNC, "Snapshot cleanup: renaming {} failed, reason: {}", block_hash, err); + } + } + // Ignore the error. Cleanup failure is not a critical error. + match fs::remove_dir_all(rename_to) { + Ok(()) => {} + Err(err) => { + cerror!(SYNC, "Snapshot cleanup: removing {} failed, reason: {}", block_hash, err); + } + } +} + +impl Drop for Service { + fn drop(&mut self) { + if let Some(canceller) = self.canceller.take() { + // The thread corresponding to the `self.join_handle` waits for the `self.canceller` is dropped. + // It must be dropped first not to make deadlock at `handle.join()`. + drop(canceller); + } + + if let Some(handle) = self.join_handle.take() { + handle.join().expect("Snapshot service thread shouldn't panic"); + } + } +} diff --git a/sync/src/snapshot/service.rs b/sync/src/snapshot/service.rs deleted file mode 100644 index e076dc3661..0000000000 --- a/sync/src/snapshot/service.rs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2018 Kodebox, Inc. -// This file is part of CodeChain. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -use std::io::ErrorKind; -use std::path::PathBuf; -use std::sync::Arc; -use std::thread::spawn; - -use ccore::{BlockChainClient, BlockChainTrait, BlockId, ChainNotify, Client, DatabaseClient}; -use ctypes::BlockHash; - -use super::error::Error; -use super::snapshot::{Snapshot, WriteSnapshot}; - -pub struct Service { - client: Arc<Client>, - /// Snapshot root directory - root_dir: String, - /// Snapshot creation period in unit of block numbers - period: u64, -} - -impl Service { - pub fn new(client: Arc<Client>, root_dir: String, period: u64) -> Arc<Self> { - Arc::new(Self { - client, - root_dir, - period, - }) - } -} - -impl ChainNotify for Service { - /// fires when chain has new blocks. - fn new_blocks( - &self, - _imported: Vec<BlockHash>, - _invalid: Vec<BlockHash>, - enacted: Vec<BlockHash>, - _retracted: Vec<BlockHash>, - _sealed: Vec<BlockHash>, - ) { - let best_number = self.client.chain_info().best_block_number; - let is_checkpoint = enacted - .iter() - .map(|hash| self.client.block_number(&BlockId::Hash(*hash)).expect("Enacted block must exist")) - .any(|number| number % self.period == 0); - if is_checkpoint && best_number > self.period { - let number = (best_number / self.period - 1) * self.period; - let header = self.client.block_header(&BlockId::Number(number)).expect("Snapshot target must exist"); - - let db = self.client.database(); - let path: PathBuf = [self.root_dir.clone(), format!("{:x}", *header.hash())].iter().collect(); - let root = header.state_root(); - // FIXME: The db can be corrupted because the CodeChain doesn't wait child threads end on exit. - spawn(move || match Snapshot::try_new(path).map(|s| s.write_snapshot(db.as_ref(), &root)) { - Ok(_) => {} - Err(Error::FileError(ErrorKind::AlreadyExists)) => {} - Err(e) => cerror!(SNAPSHOT, "{}", e), - }); - } - } -} diff --git a/sync/src/snapshot/snapshot.rs b/sync/src/snapshot/snapshot.rs deleted file mode 100644 index f5a363540f..0000000000 --- a/sync/src/snapshot/snapshot.rs +++ /dev/null @@ -1,393 +0,0 @@ -// Copyright 2018-2019 Kodebox, Inc. -// This file is part of CodeChain. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// This program 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. - -use std::collections::HashSet; -use std::convert::AsRef; -use std::fs::{create_dir_all, File}; -use std::io::{Read, Write}; -use std::iter::once; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use ccore::COL_STATE; -use cmerkle::Node; -use journaldb::{self, Algorithm, JournalDB}; -use kvdb::KeyValueDB; -use primitives::H256; -use rlp::{Rlp, RlpStream}; -use snap; - -use super::error::Error; - -pub struct Snapshot { - path: PathBuf, -} - -impl Snapshot { - pub fn try_new<P>(path: P) -> Result<Self, Error> - where - P: AsRef<Path>, { - create_dir_all(&path)?; - Ok(Snapshot { - path: path.as_ref().to_owned(), - }) - } -} - -impl Snapshot { - fn file_for(&self, root: &H256) -> PathBuf { - self.path.join(format!("{:x}", root)) - } - - fn write_nodes<'a, I>(&self, root: &H256, iter: I) -> Result<(), Error> - where - I: IntoIterator<Item = &'a (H256, Vec<u8>)>, { - let file = File::create(self.file_for(root))?; - let mut snappy = snap::Writer::new(file); - - let mut stream = RlpStream::new(); - stream.begin_unbounded_list(); - for (key, value) in iter { - stream.begin_list(2); - stream.append(key); - stream.append(value); - } - stream.complete_unbounded_list(); - - snappy.write_all(&stream.drain())?; - Ok(()) - } - - fn read_chunk(&self, backing: Arc<dyn KeyValueDB>, root: &H256) -> Result<Chunk, Error> { - let file = File::open(self.file_for(root))?; - let mut buf = Vec::new(); - let mut snappy = snap::Reader::new(file); - snappy.read_to_end(&mut buf)?; - - let rlp = Rlp::new(&buf); - let mut journal = journaldb::new(backing, Algorithm::Archive, COL_STATE); - let mut inserted_keys = HashSet::new(); - let mut referenced_keys = HashSet::new(); - referenced_keys.insert(*root); - for rlp_pair in rlp.iter() { - if rlp_pair.item_count().unwrap() != 2 { - return Err(Error::SyncError("Chunk contains invalid size of pair".to_string())) - } - - let key = rlp_pair.val_at(0).unwrap(); - let value: Vec<_> = rlp_pair.val_at(1).unwrap(); - - let node = - Node::decoded(&value).ok_or_else(|| Error::SyncError("Chunk condtains an invalid node".to_string()))?; - - if journal.contains(&key) { - cwarn!(SNAPSHOT, "Chunk contains duplicated key: {}", key); - } - - if let Node::Branch(_, childs) = node { - for child in childs.iter() { - if let Some(child) = child { - referenced_keys.insert(*child); - } - } - } - - let hash_key = journal.insert(&value); - if hash_key != key { - return Err(Error::SyncError("Chunk contains an invalid key for a value".to_string())) - } - inserted_keys.insert(hash_key); - } - - let never_referenced_keys: Vec<H256> = - inserted_keys.iter().filter(|key| !referenced_keys.contains(key)).cloned().collect(); - - Ok(Chunk { - journal, - never_referenced_keys, - }) - } -} - -struct Chunk { - journal: Box<dyn JournalDB>, - never_referenced_keys: Vec<H256>, -} - -impl Chunk { - fn purge(&mut self) -> bool { - if self.never_referenced_keys.is_empty() { - return false - } - for key in &self.never_referenced_keys { - self.journal.remove(key); - } - self.never_referenced_keys.clear(); - true - } - - fn is_deeper_than(&self, root: &H256, max_depth: usize) -> bool { - let mut stack = Vec::new(); - stack.push((*root, 0)); - while let Some((key, depth)) = stack.pop() { - match self.journal.get(&key) { - None => continue, - Some(_) if depth >= max_depth => return false, - Some(value) => { - if let Some(Node::Branch(_, childs)) = Node::decoded(&value) { - for child in childs.iter() { - if let Some(child) = child { - stack.push((*child, depth + 1)); - } - } - } - } - } - } - false - } - - fn missing_keys(&self, root: &H256) -> Vec<H256> { - let mut result = Vec::new(); - let mut stack = Vec::new(); - stack.push(*root); - while let Some(key) = stack.pop() { - match self.journal.get(&key) { - None => { - result.push(key); - } - Some(value) => { - if let Some(Node::Branch(_, childs)) = Node::decoded(&value) { - for child in childs.iter() { - if let Some(child) = child { - stack.push(*child); - } - } - } - } - } - } - result - } -} - -pub trait WriteSnapshot { - fn write_snapshot(&self, db: &dyn KeyValueDB, root: &H256) -> Result<(), Error>; -} - -pub trait ReadSnapshot { - fn read_snapshot(&self, db: Arc<dyn KeyValueDB>, root: &H256) -> Result<(), Error>; -} - -impl WriteSnapshot for Snapshot { - fn write_snapshot(&self, db: &dyn KeyValueDB, root: &H256) -> Result<(), Error> { - let root_val = match db.get(COL_STATE, root) { - Ok(Some(value)) => value.to_vec(), - Ok(None) => return Err(Error::SyncError("Invalid state root, or the database is empty".to_string())), - Err(e) => return Err(e.into()), - }; - - let children = children_of(db, &root_val)?; - let mut grandchildren = Vec::new(); - for (_, value) in &children { - grandchildren.extend(children_of(db, value)?); - } - - self.write_nodes(root, once(&(*root, root_val)).chain(&children))?; - for (grandchild, _) in &grandchildren { - let nodes = enumerate_subtree(db, grandchild)?; - self.write_nodes(grandchild, &nodes)?; - } - - Ok(()) - } -} - -impl ReadSnapshot for Snapshot { - fn read_snapshot(&self, db: Arc<dyn KeyValueDB>, root: &H256) -> Result<(), Error> { - let head = { - let mut head = self.read_chunk(db.clone(), root)?; - if head.purge() { - cinfo!(SNAPSHOT, "Head chunk contains garbages"); - } - - if head.is_deeper_than(root, 2) { - return Err(Error::SyncError("Head chunk has an invalid shape".to_string())) - } - - let mut transaction = db.transaction(); - head.journal.inject(&mut transaction)?; - db.write_buffered(transaction); - head - }; - - for chunk_root in head.missing_keys(root) { - let mut chunk = self.read_chunk(db.clone(), &chunk_root)?; - if chunk.purge() { - cinfo!(SNAPSHOT, "Chunk contains garbages"); - } - - if !chunk.missing_keys(&chunk_root).is_empty() { - return Err(Error::SyncError("Chunk is an incomplete trie".to_string())) - } - - let mut transaction = db.transaction(); - chunk.journal.inject(&mut transaction)?; - db.write_buffered(transaction); - } - - Ok(()) - } -} - -fn get_node(db: &dyn KeyValueDB, key: &H256) -> Result<Vec<u8>, Error> { - match db.get(COL_STATE, key) { - Ok(Some(value)) => Ok(value.to_vec()), - Ok(None) => Err(Error::NodeNotFound(*key)), - Err(e) => Err(e.into()), - } -} - -fn children_of(db: &dyn KeyValueDB, node: &[u8]) -> Result<Vec<(H256, Vec<u8>)>, Error> { - let keys = match Node::decoded(node) { - None => Vec::new(), - Some(Node::Leaf(..)) => Vec::new(), - Some(Node::Branch(_, children)) => children.iter().filter_map(|child| *child).collect(), - }; - - let mut result = Vec::new(); - for key in keys { - result.push((key, get_node(db, &key)?)); - } - Ok(result) -} - -fn enumerate_subtree(db: &dyn KeyValueDB, root: &H256) -> Result<Vec<(H256, Vec<u8>)>, Error> { - let node = get_node(db, root)?; - let children = match Node::decoded(&node) { - None => Vec::new(), - Some(Node::Leaf(..)) => Vec::new(), - Some(Node::Branch(_, children)) => children.iter().filter_map(|child| *child).collect(), - }; - let mut result: Vec<_> = vec![(*root, node)]; - for child in children { - result.extend(enumerate_subtree(db, &child)?); - } - Ok(result) -} - -#[cfg(test)] -mod tests { - use std::collections::HashSet; - use std::sync::Arc; - - use ccore::COL_STATE; - - use cmerkle::{Trie, TrieDB, TrieDBMut, TrieMut}; - use journaldb; - use journaldb::Algorithm; - use kvdb_memorydb; - use primitives::H256; - use tempfile::tempdir; - use trie_standardmap::{Alphabet, StandardMap, ValueMode}; - - use super::{ReadSnapshot, Snapshot, WriteSnapshot}; - - #[test] - fn init() { - let snapshot_dir = tempdir().unwrap(); - let snapshot = Snapshot::try_new(&snapshot_dir).unwrap(); - let mut root = H256::new(); - - let kvdb = Arc::new(kvdb_memorydb::create(1)); - let mut jdb = journaldb::new(kvdb.clone(), Algorithm::Archive, COL_STATE); - { - let _ = TrieDBMut::new(jdb.as_hashdb_mut(), &mut root); - } - /* do nothing */ - let result = snapshot.write_snapshot(kvdb.as_ref(), &root); - - assert!(result.is_err()); - } - - fn random_insert_and_restore_with_count(count: usize) { - let mut seed = H256::new(); - let x = StandardMap { - alphabet: Alphabet::Custom(b"@QWERTYUIOPASDFGHJKLZXCVBNM[/]^_".to_vec()), - min_key: 5, - journal_key: 0, - value_mode: ValueMode::Index, - count, - } - .make_with(&mut seed); - - let snapshot_dir = tempdir().unwrap(); - let snapshot = Snapshot::try_new(&snapshot_dir).unwrap(); - let mut root = H256::new(); - { - let kvdb = Arc::new(kvdb_memorydb::create(1)); - let mut jdb = journaldb::new(kvdb.clone(), Algorithm::Archive, COL_STATE); - { - let mut t = TrieDBMut::new(jdb.as_hashdb_mut(), &mut root); - let mut inserted_keys = HashSet::new(); - for &(ref key, ref value) in &x { - if !inserted_keys.insert(key) { - continue - } - assert!(t.insert(key, value).unwrap().is_none()); - assert_eq!(t.insert(key, value).unwrap(), Some(value.to_vec())); - } - } - { - let mut batch = jdb.backing().transaction(); - let _ = jdb.inject(&mut batch).unwrap(); - jdb.backing().write(batch).unwrap(); - } - - snapshot.write_snapshot(kvdb.as_ref(), &root).unwrap(); - } - - { - let kvdb = Arc::new(kvdb_memorydb::create(1)); - snapshot.read_snapshot(kvdb.clone(), &root).unwrap(); - - let mut jdb = journaldb::new(kvdb, Algorithm::Archive, COL_STATE); - let t = TrieDB::try_new(jdb.as_hashdb_mut(), &root).unwrap(); - let mut inserted_keys = HashSet::new(); - for &(ref key, ref value) in &x { - if !inserted_keys.insert(key) { - continue - } - assert_eq!(t.get(key).unwrap(), Some(value.to_vec())); - } - } - } - - #[test] - fn random_insert_and_restore_1() { - random_insert_and_restore_with_count(1); - } - - #[test] - fn random_insert_and_restore_100() { - random_insert_and_restore_with_count(100); - } - - #[test] - fn random_insert_and_restore_10000() { - random_insert_and_restore_with_count(10000); - } -} diff --git a/test/src/e2e.dynval/2/snapshot.test.ts b/test/src/e2e.dynval/2/snapshot.test.ts new file mode 100644 index 0000000000..bb706a2c45 --- /dev/null +++ b/test/src/e2e.dynval/2/snapshot.test.ts @@ -0,0 +1,187 @@ +// Copyright 2019 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +import * as chai from "chai"; +import { expect } from "chai"; +import * as chaiAsPromised from "chai-as-promised"; +import * as stake from "codechain-stakeholder-sdk"; +import * as fs from "fs"; +import "mocha"; +import * as path from "path"; + +import mkdirp = require("mkdirp"); +import { validators } from "../../../tendermint.dynval/constants"; +import { faucetAddress, faucetSecret } from "../../helper/constants"; +import { PromiseExpect } from "../../helper/promise"; +import CodeChain, { Signer } from "../../helper/spawn"; +import { setTermTestTimeout, withNodes } from "../setup"; + +chai.use(chaiAsPromised); + +const SNAPSHOT_CONFIG = `${__dirname}/../../../tendermint.dynval/snapshot-config.yml`; +const SNAPSHOT_PATH = `${__dirname}/../../../../snapshot/`; + +describe("Snapshot for Tendermint with Dynamic Validator", function() { + const promiseExpect = new PromiseExpect(); + const snapshotValidators = validators.slice(0, 3); + const freshNodeValidator = validators[3]; + const { nodes } = withNodes(this, { + promiseExpect, + overrideParams: { + maxNumOfValidators: 3, + era: 1 + }, + validators: snapshotValidators.map((signer, index) => ({ + signer, + delegation: 5000, + deposit: 10_000_000 - index // tie-breaker + })), + modify: () => { + mkdirp.sync(SNAPSHOT_PATH); + const snapshotPath = fs.mkdtempSync(SNAPSHOT_PATH); + return { + additionalArgv: [ + "--snapshot-path", + snapshotPath, + "--config", + SNAPSHOT_CONFIG + ], + nodeAdditionalProperties: { + snapshotPath + } + }; + } + }); + + it("should be exist after some time", async function() { + const termWaiter = setTermTestTimeout(this, { + terms: 2 + }); + const termMetadata = await termWaiter.waitNodeUntilTerm(nodes[0], { + target: 2, + termPeriods: 1 + }); + const snapshotBlock = await getSnapshotBlock(nodes[0], termMetadata); + expect( + path.join( + nodes[0].snapshotPath, + snapshotBlock.hash.toString(), + snapshotBlock.stateRoot.toString() + ) + ).to.satisfy(fs.existsSync); + }); + + it("should be able to boot with the snapshot", async function() { + const termWaiter = setTermTestTimeout(this, { + terms: 3 + }); + const termMetadata1 = await termWaiter.waitNodeUntilTerm(nodes[0], { + target: 2, + termPeriods: 1 + }); + const snapshotBlock = await getSnapshotBlock(nodes[0], termMetadata1); + await makeItValidator(nodes[0], freshNodeValidator); + const snapshotPath = fs.mkdtempSync(SNAPSHOT_PATH); + const node = new CodeChain({ + chain: `${__dirname}/../../scheme/tendermint-dynval.json`, + argv: [ + "--engine-signer", + freshNodeValidator.platformAddress.toString(), + "--password-path", + `test/tendermint.dynval/${freshNodeValidator.platformAddress.value}/password.json`, + "--force-sealing", + "--snapshot-path", + snapshotPath, + "--config", + SNAPSHOT_CONFIG, + "--snapshot-hash", + snapshotBlock.hash.toString(), + "--snapshot-number", + snapshotBlock.number.toString() + ], + additionalKeysPath: `tendermint.dynval/${freshNodeValidator.platformAddress.value}/keys` + }); + try { + await node.start(); + await node.connect(nodes[0]); + await termWaiter.waitNodeUntilTerm(node, { + target: 4, + termPeriods: 2 + }); + + // Check that the freshNodeValidator is still a validator & make sure it doesn't have a block/header before termMetadata1. + } catch (e) { + node.keepLogs(); + throw e; + } finally { + await node.clean(); + } + }); + + afterEach(async function() { + promiseExpect.checkFulfilled(); + }); +}); + +async function getSnapshotBlock( + node: CodeChain, + termMetadata: stake.TermMetadata +) { + const blockNumber = termMetadata.lastTermFinishedBlockNumber + 1; + await node.waitBlockNumber(blockNumber); + return (await node.sdk.rpc.chain.getBlock(blockNumber))!; +} + +async function makeItValidator(node: CodeChain, freshNodeValidator: Signer) { + const faucetSeq = await node.sdk.rpc.chain.getSeq(faucetAddress); + const payTx = node.sdk.core + .createPayTransaction({ + recipient: freshNodeValidator.platformAddress, + quantity: 200000000 + }) + .sign({ + secret: faucetSecret, + seq: faucetSeq, + fee: 10 + }); + await node.waitForTx(await node.sdk.rpc.chain.sendSignedTransaction(payTx)); + const selfNominateTx = stake + .createSelfNominateTransaction(node.sdk, 10000000, "") + .sign({ + secret: freshNodeValidator.privateKey, + seq: await node.sdk.rpc.chain.getSeq( + freshNodeValidator.platformAddress + ), + fee: 10 + }); + await node.waitForTx( + await node.sdk.rpc.chain.sendSignedTransaction(selfNominateTx) + ); + const delegateTx = stake + .createDelegateCCSTransaction( + node.sdk, + freshNodeValidator.platformAddress, + 5000 + ) + .sign({ + secret: faucetSecret, + seq: faucetSeq + 1, + fee: 10 + }); + await node.waitForTx( + await node.sdk.rpc.chain.sendSignedTransaction(delegateTx) + ); +} diff --git a/test/src/e2e.dynval/setup.ts b/test/src/e2e.dynval/setup.ts index 493f6ef602..6cc695e2fb 100644 --- a/test/src/e2e.dynval/setup.ts +++ b/test/src/e2e.dynval/setup.ts @@ -39,17 +39,29 @@ interface ValidatorConfig { delegation?: U64Value; } -export function withNodes( +interface NodePropertyModifier<T> { + additionalArgv: string[]; + nodeAdditionalProperties: T; +} + +export function withNodes<T>( suite: Suite, options: { promiseExpect: PromiseExpect; validators: ValidatorConfig[]; overrideParams?: Partial<CommonParams>; onBeforeEnable?: (nodes: CodeChain[]) => Promise<void>; + modify?: (signer: Signer, index: number) => NodePropertyModifier<T>; } ) { - const nodes: CodeChain[] = []; - const { overrideParams = {} } = options; + const nodes: (CodeChain & T)[] = []; + const { + overrideParams = {}, + modify = () => ({ + additionalArgv: [], + nodeAdditionalProperties: {} as T + }) + } = options; const initialParams = { ...defaultParams, ...overrideParams @@ -62,7 +74,8 @@ export function withNodes( nodes.length = 0; const newNodes = await createNodes({ ...options, - initialParams + initialParams, + modify }); nodes.push(...newNodes); }); @@ -95,14 +108,15 @@ export function findNode(nodes: CodeChain[], signer: Signer) { ); } -async function createNodes(options: { +async function createNodes<T>(options: { promiseExpect: PromiseExpect; validators: ValidatorConfig[]; initialParams: CommonParams; onBeforeEnable?: (nodes: CodeChain[]) => Promise<void>; -}): Promise<CodeChain[]> { + modify: (signer: Signer, index: number) => NodePropertyModifier<T>; +}): Promise<(CodeChain & T)[]> { const chain = `${__dirname}/../scheme/tendermint-dynval.json`; - const { promiseExpect, validators, initialParams } = options; + const { promiseExpect, validators, initialParams, modify } = options; const initialNodes: CodeChain[] = []; const initialValidators = [ @@ -124,20 +138,23 @@ async function createNodes(options: { }); } - const nodes: CodeChain[] = []; + const nodes: (CodeChain & T)[] = []; for (let i = 0; i < validators.length; i++) { const { signer: validator } = validators[i]; - nodes[i] = new CodeChain({ + const modifier = modify(validator, i); + const node = new CodeChain({ chain, argv: [ "--engine-signer", validator.platformAddress.value, "--password-path", `test/tendermint.dynval/${validator.platformAddress.value}/password.json`, - "--force-sealing" + "--force-sealing", + ...modifier.additionalArgv ], additionalKeysPath: `tendermint.dynval/${validator.platformAddress.value}/keys` }); + nodes[i] = Object.assign(node, modifier.nodeAdditionalProperties); nodes[i].signer = validator; } let bootstrapFailed = false; @@ -451,7 +468,7 @@ interface TermWaiter { target: number; termPeriods: number; } - ): Promise<void>; + ): Promise<stake.TermMetadata>; } export function setTermTestTimeout( @@ -483,7 +500,7 @@ export function setTermTestTimeout( termPeriods: number; } ) { - await node.waitForTermChange( + return await node.waitForTermChange( waiterParams.target, termPeriodsToTime(waiterParams.termPeriods, 0.5) ); diff --git a/test/src/e2e/snapshot.test.ts b/test/src/e2e/snapshot.test.ts new file mode 100644 index 0000000000..195cf00c8a --- /dev/null +++ b/test/src/e2e/snapshot.test.ts @@ -0,0 +1,120 @@ +// Copyright 2018-2019 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +import { expect } from "chai"; +import * as fs from "fs"; +import "mocha"; +import * as path from "path"; + +import { aliceAddress } from "../helper/constants"; +import CodeChain from "../helper/spawn"; + +const SNAPSHOT_PATH = `${__dirname}/../../../snapshot/`; + +describe("Snapshot", async function() { + let node: CodeChain; + before(async function() { + node = new CodeChain({ + argv: ["--snapshot-path", SNAPSHOT_PATH] + }); + await node.start(); + }); + + it("can make a snapshot when it is requsted with devel rpc", async function() { + const pay = await node.sendPayTx({ + quantity: 100, + recipient: aliceAddress + }); + + const blockHash = (await node.sdk.rpc.chain.getTransaction(pay.hash()))! + .blockHash!; + await node.sdk.rpc.sendRpcRequest("devel_snapshot", [ + blockHash.toJSON() + ]); + // Wait for 1 secs + await new Promise(resolve => setTimeout(resolve, 1000)); + + const stateRoot = (await node.sdk.rpc.chain.getBlock(blockHash))! + .stateRoot; + expect( + path.join(SNAPSHOT_PATH, blockHash.toString(), stateRoot.toString()) + ).to.satisfies(fs.existsSync); + }); + + it("can restore from snapshot", async function() { + for (let i = 0; i < 10; i++) { + const tx = await node.sendPayTx({ + quantity: 100, + recipient: aliceAddress + }); + await node.waitForTx(tx.hash()); + } + + const pay = await node.sendPayTx({ + quantity: 100, + recipient: aliceAddress + }); + + const blockHash = (await node.sdk.rpc.chain.getTransaction(pay.hash()))! + .blockHash!; + + const block = (await node.sdk.rpc.chain.getBlock(blockHash))!; + await node.sdk.rpc.sendRpcRequest("devel_snapshot", [ + blockHash.toJSON() + ]); + // Wait for 1 secs + await new Promise(resolve => setTimeout(resolve, 1000)); + + const newNode = new CodeChain({ + argv: [ + "--snapshot-hash", + block.hash.toString(), + "--snapshot-number", + block.number.toString() + ] + }); + + try { + await newNode.start(); + await newNode.connect(node); + await newNode.waitBlockNumber(block.number); + await node.sdk.rpc.devel.stopSealing(); + // New node creates block + const newPay = await newNode.sendPayTx({ + quantity: 100, + recipient: aliceAddress + }); + await newNode.waitForTx(newPay.hash()); + await node.sdk.rpc.devel.startSealing(); + await node.waitForTx(newPay.hash()); + } catch (e) { + newNode.keepLogs(); + throw e; + } finally { + await newNode.clean(); + } + }); + + afterEach(function() { + if (this.currentTest!.state === "failed") { + node.keepLogs(); + } + }); + + after(async function() { + await node.clean(); + }); +}); diff --git a/test/src/helper/mock/blockSyncMessage.ts b/test/src/helper/mock/blockSyncMessage.ts index d064d825b4..19385edd0d 100644 --- a/test/src/helper/mock/blockSyncMessage.ts +++ b/test/src/helper/mock/blockSyncMessage.ts @@ -64,7 +64,9 @@ export class BlockSyncMessage { if (msgId === MessageType.MESSAGE_ID_STATUS) { Emitter.emit("status"); const msg = decodedmsg[1]; - const totalScore = new U256(parseInt(msg[0].toString("hex"), 16)); + const totalScore = new U256( + parseInt(msg[0].toString("hex"), 16) || 0 + ); const bestHash = new H256(msg[1].toString("hex")); const genesisHash = new H256(msg[2].toString("hex")); return new BlockSyncMessage({ diff --git a/test/src/helper/spawn.ts b/test/src/helper/spawn.ts index 4b2eb4bc95..f2d1cf396b 100644 --- a/test/src/helper/spawn.ts +++ b/test/src/helper/spawn.ts @@ -859,14 +859,16 @@ export default class CodeChain { public async waitForTermChange(target: number, timeout?: number) { const start = Date.now(); while (true) { - const termMetadata = await stake.getTermMetadata(this.sdk); + const termMetadata = (await stake.getTermMetadata(this.sdk))!; if (termMetadata && termMetadata.currentTermId >= target) { - break; + return termMetadata; } await wait(1000); if (timeout) { if (Date.now() - start > timeout * 1000) { - throw new Error(`Term didn't changed in ${timeout} s`); + throw new Error( + `Term didn't changed to ${target} in ${timeout} s. It is ${termMetadata.currentTermId} now` + ); } } } diff --git a/test/tendermint.dynval/snapshot-config.yml b/test/tendermint.dynval/snapshot-config.yml new file mode 100644 index 0000000000..9f5c890280 --- /dev/null +++ b/test/tendermint.dynval/snapshot-config.yml @@ -0,0 +1,19 @@ +[codechain] + +[mining] + +[network] + +[rpc] + +[ipc] + +[ws] + +[snapshot] +disable = false + +[stratum] + +[email_alarm] + diff --git a/util/merkle/Cargo.toml b/util/merkle/Cargo.toml index deb92626de..f95f3f8005 100644 --- a/util/merkle/Cargo.toml +++ b/util/merkle/Cargo.toml @@ -8,8 +8,11 @@ edition = "2018" rand = "0.6.1" hashdb = {path = "../hashdb" } codechain-crypto = { git = "https://github.com/CodeChain-io/rust-codechain-crypto.git", version = "0.1" } +memorydb = { path = "../memorydb" } primitives = { git = "https://github.com/CodeChain-io/rust-codechain-primitives.git", version = "0.4" } rlp = { git = "https://github.com/CodeChain-io/rlp.git", version = "0.4" } +rlp_derive = { path = "../rlp_derive" } +snap = "0.2" [dev-dependencies] journaldb = { path = "../journaldb" } diff --git a/util/merkle/src/lib.rs b/util/merkle/src/lib.rs index 80acf9399a..cf40cc9e46 100644 --- a/util/merkle/src/lib.rs +++ b/util/merkle/src/lib.rs @@ -20,6 +20,9 @@ extern crate hashdb; extern crate memorydb; extern crate primitives; extern crate rlp; +#[macro_use] +extern crate rlp_derive; +extern crate snap; #[cfg(test)] extern crate trie_standardmap as standardmap; @@ -33,6 +36,8 @@ use primitives::H256; mod nibbleslice; pub mod node; mod skewed; +#[allow(dead_code)] +pub mod snapshot; pub mod triedb; pub mod triedbmut; pub mod triehash; diff --git a/util/merkle/src/nibbleslice.rs b/util/merkle/src/nibbleslice.rs index d39714c21d..d979860809 100644 --- a/util/merkle/src/nibbleslice.rs +++ b/util/merkle/src/nibbleslice.rs @@ -17,7 +17,7 @@ use std::cmp::*; use std::fmt; -#[derive(Eq, Ord)] +#[derive(Eq, Ord, Copy, Clone)] pub struct NibbleSlice<'a> { pub data: &'a [u8], pub offset: usize, diff --git a/util/merkle/src/node.rs b/util/merkle/src/node.rs index 66f2704808..4d556860d6 100644 --- a/util/merkle/src/node.rs +++ b/util/merkle/src/node.rs @@ -112,4 +112,11 @@ impl<'a> Node<'a> { } } } + + pub fn mid(self, offset: usize) -> Self { + match self { + Node::Leaf(partial, value) => Node::Leaf(partial.mid(offset), value), + Node::Branch(partial, child) => Node::Branch(partial.mid(offset), child), + } + } } diff --git a/util/merkle/src/snapshot/chunk.rs b/util/merkle/src/snapshot/chunk.rs new file mode 100644 index 0000000000..40ee320c13 --- /dev/null +++ b/util/merkle/src/snapshot/chunk.rs @@ -0,0 +1,284 @@ +// Copyright 2019 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use std::collections::VecDeque; +use std::convert::From; + +use ccrypto::BLAKE_NULL_RLP; +use hashdb::{DBValue, HashDB}; +use primitives::H256; + +use super::error::{ChunkError, Error}; +use super::{DecodedPathSlice, PathSlice, CHUNK_HEIGHT}; +use crate::nibbleslice::NibbleSlice; +use crate::{Node, TrieDBMut}; + +#[derive(RlpEncodable, RlpDecodable, Eq, PartialEq)] +pub struct TerminalNode { + // Relative path from the chunk root. + pub path_slice: PathSlice, + pub node_rlp: Vec<u8>, +} + +impl std::fmt::Debug for TerminalNode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let path_slice = NibbleSlice::from_encoded(&self.path_slice); + f.debug_struct("TerminalNode") + .field("path_slice", &path_slice) + .field("node_rlp", &NodeDebugAdaptor { + rlp: &self.node_rlp, + }) + .finish() + } +} + +struct NodeDebugAdaptor<'a> { + rlp: &'a [u8], +} + +impl<'a> std::fmt::Debug for NodeDebugAdaptor<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + match Node::decoded(&self.rlp) { + Some(node) => write!(f, "{:?}", &node), + None => write!(f, "{:?}", self.rlp), + } + } +} + +/// An unverified chunk from the network +#[derive(Debug)] +pub struct RawChunk { + pub nodes: Vec<TerminalNode>, +} + +/// Fully recovered, and re-hydrated chunk. +pub struct RecoveredChunk { + pub(crate) root: H256, + /// contains all nodes including non-terminal nodes and terminal nodes. + /// You can blindly pour all items in `nodes` into `HashDB`. + pub(crate) nodes: Vec<(H256, DBValue)>, + /// Their path slices are relative to this chunk root. + pub(crate) unresolved_chunks: Vec<UnresolvedChunk>, +} + +impl RawChunk { + /// Verify and recover the chunk + pub fn recover(&self, expected_chunk_root: H256) -> Result<RecoveredChunk, Error> { + let mut memorydb = memorydb::MemoryDB::new(); + let mut chunk_root = H256::new(); + + { + let mut trie = TrieDBMut::new(&mut memorydb, &mut chunk_root); + for node in self.nodes.iter() { + let old_val = match Node::decoded(&node.node_rlp) { + Some(Node::Branch(slice, child)) => { + let encoded = DecodedPathSlice::from_encoded(&node.path_slice).with_slice(slice).encode(); + trie.insert_raw(Node::Branch(NibbleSlice::from_encoded(&encoded), child))? + } + Some(Node::Leaf(slice, data)) => { + let encoded = DecodedPathSlice::from_encoded(&node.path_slice).with_slice(slice).encode(); + trie.insert_raw(Node::Leaf(NibbleSlice::from_encoded(&encoded), data))? + } + None => return Err(ChunkError::InvalidContent.into()), + }; + + if let Some(old_val) = old_val { + if old_val != node.node_rlp.as_slice() { + return Err(ChunkError::InvalidContent.into()) + } + } + } + } + + // Some nodes in the chunk is different from the expected. + if chunk_root != expected_chunk_root { + return Err(ChunkError::ChunkRootMismatch { + expected: expected_chunk_root, + actual: chunk_root, + } + .into()) + } + + let mut nodes = Vec::new(); + let mut unresolved_chunks = Vec::new(); + let mut queue: VecDeque<NodePath> = VecDeque::from(vec![NodePath::new(chunk_root)]); + while let Some(path) = queue.pop_front() { + let node = match memorydb.get(&path.key) { + Some(x) => x, + None => { + // all unresolved should depth == CHUNK_HEIGHT + 1 + if path.depth != CHUNK_HEIGHT + 1 { + return Err(ChunkError::InvalidHeight.into()) + } + + unresolved_chunks.push(UnresolvedChunk::from(path)); + continue + } + }; + + if path.depth > CHUNK_HEIGHT { + return Err(ChunkError::InvalidHeight.into()) + } + nodes.push((path.key, node.clone())); + + let node = Node::decoded(&node).expect("Chunk root was verified; Node can't be wrong"); + if let Node::Branch(slice, children) = node { + for (index, child) in children.iter().enumerate() { + if let Some(child) = child { + queue.push_back(path.with_slice_and_index(slice, index, *child)); + } + } + } + } + + Ok(RecoveredChunk { + root: expected_chunk_root, + nodes, + unresolved_chunks, + }) + } +} + +impl std::fmt::Debug for RecoveredChunk { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + struct Adapter<'a>(&'a [(H256, DBValue)]); + impl<'a> std::fmt::Debug for Adapter<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_list() + .entries(self.0.iter().map(|(hash, rlp)| { + (hash, NodeDebugAdaptor { + rlp, + }) + })) + .finish() + } + } + + f.debug_struct("RecoveredChunk") + .field("root", &self.root) + .field("nodes", &Adapter(&self.nodes)) + .field("unresolved_chunks", &self.unresolved_chunks) + .finish() + } +} + +/// Chunk obtained from the state db. +#[derive(Debug)] +pub struct Chunk { + pub root: H256, + pub terminal_nodes: Vec<TerminalNode>, +} + +impl Chunk { + pub(crate) fn from_chunk_root(db: &dyn HashDB, chunk_root: H256) -> Chunk { + let mut unresolved: VecDeque<NodePath> = VecDeque::from(vec![NodePath::new(chunk_root)]); + let mut terminal_nodes: Vec<TerminalNode> = Vec::new(); + while let Some(path) = unresolved.pop_front() { + assert!(path.key != BLAKE_NULL_RLP, "Empty DB"); + assert!(path.depth <= CHUNK_HEIGHT); + let node = db.get(&path.key).expect("Can't find the node in a db. DB is inconsistent"); + let node_decoded = Node::decoded(&node).expect("Node cannot be decoded. DB is inconsistent"); + + match node_decoded { + // Continue to BFS + Node::Branch(slice, ref children) if path.depth < CHUNK_HEIGHT => { + for (i, hash) in children.iter().enumerate() { + if let Some(hash) = hash { + unresolved.push_back(path.with_slice_and_index(slice, i, *hash)); + } + } + } + // Reached the terminal node. Branch at path.depth == CHUNK_HEIGHT || Leaf + _ => terminal_nodes.push(TerminalNode { + path_slice: path.path_slice.encode(), + node_rlp: node.to_vec(), + }), + }; + } + Chunk { + root: chunk_root, + terminal_nodes, + } + } + + // Returns path slices to unresolved chunk roots relative to this chunk root + pub(crate) fn unresolved_chunks(&self) -> Vec<UnresolvedChunk> { + let mut result = Vec::new(); + for node in self.terminal_nodes.iter() { + let decoded = Node::decoded(&node.node_rlp).expect("All terminal nodes should be valid"); + if let Node::Branch(slice, children) = decoded { + for (i, child) in children.iter().enumerate() { + if let Some(child) = child { + result.push(UnresolvedChunk { + path_slice: DecodedPathSlice::from_encoded(&node.path_slice).with_slice_and_index(slice, i), + chunk_root: *child, + }) + } + } + } + } + result + } + + #[cfg(test)] + pub(crate) fn into_raw_chunk(self) -> RawChunk { + RawChunk { + nodes: self.terminal_nodes, + } + } +} + +/// path slice to `chunk_root` is relative to the root of originating chunk. +#[derive(Debug)] +pub(crate) struct UnresolvedChunk { + pub path_slice: DecodedPathSlice, + pub chunk_root: H256, +} + +impl From<NodePath> for UnresolvedChunk { + fn from(path: NodePath) -> Self { + Self { + path_slice: path.path_slice, + chunk_root: path.key, + } + } +} + +#[derive(Debug)] +struct NodePath { + // path slice to the node relative to chunk_root + path_slice: DecodedPathSlice, + depth: usize, + key: H256, +} + +impl NodePath { + fn new(key: H256) -> NodePath { + NodePath { + path_slice: DecodedPathSlice::new(), + depth: 1, + key, + } + } + + fn with_slice_and_index(&self, slice: NibbleSlice, index: usize, key: H256) -> NodePath { + NodePath { + path_slice: self.path_slice.with_slice_and_index(slice, index), + depth: self.depth + 1, + key, + } + } +} diff --git a/util/merkle/src/snapshot/compress.rs b/util/merkle/src/snapshot/compress.rs new file mode 100644 index 0000000000..e5733baf2a --- /dev/null +++ b/util/merkle/src/snapshot/compress.rs @@ -0,0 +1,119 @@ +// Copyright 2019 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use std::io::{Cursor, Read, Write}; + +use rlp::{Rlp, RlpStream}; + +use super::chunk::{Chunk, RawChunk}; +use super::error::{ChunkError, Error}; +use super::CHUNK_MAX_NODES; + +pub struct ChunkDecompressor<R> { + read: R, +} + +impl<R> ChunkDecompressor<R> { + pub fn new(read: R) -> Self { + ChunkDecompressor { + read, + } + } +} + +impl<'a> ChunkDecompressor<Cursor<&'a [u8]>> { + pub fn from_slice(slice: &'a [u8]) -> Self { + ChunkDecompressor::new(Cursor::new(slice)) + } +} + +impl<R> ChunkDecompressor<R> +where + R: Read + Clone, +{ + pub fn decompress(self) -> Result<RawChunk, Error> { + let mut buf = Vec::new(); + + let mut snappy = snap::Reader::new(self.read); + snappy.read_to_end(&mut buf)?; + + let rlp = Rlp::new(&buf); + let len = rlp.item_count()?; + if len > CHUNK_MAX_NODES { + return Err(ChunkError::TooBig.into()) + } + + Ok(RawChunk { + nodes: rlp.as_list()?, + }) + } +} + +pub struct ChunkCompressor<W> { + write: W, +} + +impl<W> ChunkCompressor<W> { + pub fn new(write: W) -> Self { + ChunkCompressor { + write, + } + } +} + +impl<W> ChunkCompressor<W> +where + W: Write, +{ + pub fn compress_chunk(self, chunk: &Chunk) -> Result<(), Error> { + let mut rlp = RlpStream::new_list(chunk.terminal_nodes.len()); + for node in chunk.terminal_nodes.iter() { + rlp.append(node); + } + let mut snappy = snap::Writer::new(self.write); + snappy.write_all(rlp.as_raw())?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::snapshot::chunk::{Chunk, TerminalNode}; + + #[test] + fn test_compress_decompress() { + let chunk = Chunk { + root: Default::default(), + terminal_nodes: vec![ + (TerminalNode { + path_slice: b"12345".to_vec(), + node_rlp: b"45678".to_vec(), + }), + (TerminalNode { + path_slice: b"56789".to_vec(), + node_rlp: b"123abc".to_vec(), + }), + ], + }; + + let mut buffer = Vec::new(); + ChunkCompressor::new(&mut buffer).compress_chunk(&chunk).unwrap(); + let decompressed = ChunkDecompressor::from_slice(&buffer).decompress().unwrap(); + + assert_eq!(chunk.terminal_nodes, decompressed.nodes); + } +} diff --git a/util/merkle/src/snapshot/error.rs b/util/merkle/src/snapshot/error.rs new file mode 100644 index 0000000000..5077312818 --- /dev/null +++ b/util/merkle/src/snapshot/error.rs @@ -0,0 +1,91 @@ +// Copyright 2019 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use std::io::Error as IoError; + +use primitives::H256; +use rlp::DecoderError as RlpDecoderError; + +use crate::TrieError; +use std::fmt::{Display, Formatter}; + +#[derive(Debug)] +pub enum Error { + IoError(IoError), + RlpDecoderError(RlpDecoderError), + TrieError(TrieError), + ChunkError(ChunkError), +} + +impl From<IoError> for Error { + fn from(err: IoError) -> Self { + Error::IoError(err) + } +} + +impl From<RlpDecoderError> for Error { + fn from(err: RlpDecoderError) -> Self { + Error::RlpDecoderError(err) + } +} + +impl From<TrieError> for Error { + fn from(err: TrieError) -> Self { + Error::TrieError(err) + } +} + +impl From<ChunkError> for Error { + fn from(err: ChunkError) -> Self { + Error::ChunkError(err) + } +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + Error::IoError(err) => write!(f, "IoError: {}", err), + Error::RlpDecoderError(err) => write!(f, "RlpDecoderError: {}", err), + Error::TrieError(err) => write!(f, "TrieError: {}", err), + Error::ChunkError(err) => write!(f, "ChunkError: {}", err), + } + } +} + +#[derive(Debug)] +pub enum ChunkError { + TooBig, + InvalidHeight, + ChunkRootMismatch { + expected: H256, + actual: H256, + }, + InvalidContent, +} + +impl Display for ChunkError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + match self { + ChunkError::TooBig => write!(f, "Chunk has too many elements"), + ChunkError::InvalidHeight => write!(f, "Chunk height is unexpected height"), + ChunkError::ChunkRootMismatch { + expected, + actual, + } => write!(f, "Chunk root is different from expected. expected: {}, actual: {}", expected, actual), + ChunkError::InvalidContent => write!(f, "Chunk content is invalid"), + } + } +} diff --git a/util/merkle/src/snapshot/mod.rs b/util/merkle/src/snapshot/mod.rs new file mode 100644 index 0000000000..860fc71d94 --- /dev/null +++ b/util/merkle/src/snapshot/mod.rs @@ -0,0 +1,343 @@ +// Copyright 2019 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +pub mod chunk; +mod compress; +mod error; +mod ordered_heap; + +use std::cmp::Ordering; + +use ccrypto::BLAKE_NULL_RLP; +use hashdb::HashDB; +use primitives::H256; + +use self::chunk::{Chunk, RecoveredChunk, UnresolvedChunk}; +pub use self::compress::{ChunkCompressor, ChunkDecompressor}; +pub use self::error::Error; +use self::ordered_heap::OrderedHeap; +use crate::nibbleslice::NibbleSlice; + +const CHUNK_HEIGHT: usize = 3; +const CHUNK_MAX_NODES: usize = 256; // 16 ^ (CHUNK_HEIGHT-1) + +/// Example: +/// use codechain_merkle::snapshot::Restore; +/// let mut rm = Restore::new(root); +/// while let Some(root) = rm.next_to_feed() { +/// let raw_chunk = request(block_hash, root)?; +/// let chunk = raw_chunk.recover(root)?; +/// rm.feed(db, chunk); +/// } +pub struct Restore { + pending: Option<ChunkPathPrefix>, + unresolved: OrderedHeap<DepthFirst<ChunkPathPrefix>>, +} + +impl Restore { + pub fn new(merkle_root: H256) -> Self { + let mut result = Restore { + pending: None, + unresolved: OrderedHeap::new(), + }; + if merkle_root != BLAKE_NULL_RLP { + result.unresolved.push(ChunkPathPrefix::new(merkle_root).into()); + } + result + } + + pub fn feed(&mut self, db: &mut dyn HashDB, chunk: RecoveredChunk) { + let pending_path = self.pending.take().expect("feed() should be called after next()"); + assert_eq!(pending_path.chunk_root, chunk.root, "Unexpected chunk"); + + // Pour nodes into the DB + for (key, value) in chunk.nodes { + db.emplace(key, value); + } + + // Extend search paths + for unresolved in chunk.unresolved_chunks { + self.unresolved.push(pending_path.with_unresolved_chunk(&unresolved).into()); + } + + self.pending = None; + } + + pub fn next_to_feed(&mut self) -> Option<H256> { + if let Some(pending) = &self.pending { + Some(pending.chunk_root) + } else if let Some(path) = self.unresolved.pop() { + let chunk_root = path.chunk_root; + self.pending = Some(path.0); + + Some(chunk_root) + } else { + None + } + } +} + +impl std::fmt::Debug for Restore { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("Restore").field("pending", &self.pending).field("unresolved", &"<...>".to_string()).finish() + } +} + +/// Example: +/// use std::fs::File; +/// use codechain_merkle::snapshot::Snapshot; +/// +/// for chunk in Snapshot::from_hashdb(db, root) { +/// let mut file = File::create(format!("{}/{}", block_id, chunk.root))?; +/// let mut compressor = ChunkCompressor::new(&mut file); +/// compressor.compress(chunk); +/// } +pub struct Snapshot<'a> { + db: &'a dyn HashDB, + remaining: OrderedHeap<DepthFirst<ChunkPathPrefix>>, +} + +impl<'a> Snapshot<'a> { + pub fn from_hashdb(db: &'a dyn HashDB, chunk_root: H256) -> Self { + let mut result = Snapshot { + db, + remaining: OrderedHeap::new(), + }; + if chunk_root != BLAKE_NULL_RLP { + result.remaining.push(ChunkPathPrefix::new(chunk_root).into()); + } + result + } +} + +impl<'a> Iterator for Snapshot<'a> { + type Item = Chunk; + + fn next(&mut self) -> Option<Self::Item> { + if let Some(path) = self.remaining.pop() { + let chunk = Chunk::from_chunk_root(self.db, path.chunk_root); + for unresolved in chunk.unresolved_chunks() { + self.remaining.push(path.with_unresolved_chunk(&unresolved).into()); + } + Some(chunk) + } else { + None + } + } +} + + +#[derive(Debug)] +struct ChunkPathPrefix { + // Absolute path prefix of the chunk root + path_prefix: DecodedPathSlice, + depth: usize, + chunk_root: H256, +} + +impl ChunkPathPrefix { + fn new(chunk_root: H256) -> ChunkPathPrefix { + ChunkPathPrefix { + path_prefix: DecodedPathSlice::new(), + depth: 1, + chunk_root, + } + } + + fn with_unresolved_chunk(&self, unresolved: &UnresolvedChunk) -> ChunkPathPrefix { + ChunkPathPrefix { + path_prefix: self.path_prefix.with_path_slice(&unresolved.path_slice), + depth: self.depth + 1, + chunk_root: unresolved.chunk_root, + } + } +} + +impl Ord for DepthFirst<ChunkPathPrefix> { + fn cmp(&self, other: &Self) -> Ordering { + self.0.depth.cmp(&other.0.depth) + } +} + +impl From<ChunkPathPrefix> for DepthFirst<ChunkPathPrefix> { + fn from(path: ChunkPathPrefix) -> Self { + DepthFirst(path) + } +} + +/// Encoded value by NibbleSlice::encoded() +pub type PathSlice = Vec<u8>; + +/// for item i, i in 0..16 +pub(crate) struct DecodedPathSlice(Vec<u8>); + +impl DecodedPathSlice { + fn new() -> DecodedPathSlice { + DecodedPathSlice(Vec::new()) + } + + fn from_encoded(slice: &[u8]) -> DecodedPathSlice { + DecodedPathSlice(NibbleSlice::from_encoded(slice).to_vec()) + } + + fn with_slice_and_index(&self, slice: NibbleSlice, i: usize) -> DecodedPathSlice { + assert!(i < 16); + let mut v = self.0.clone(); + v.append(&mut slice.to_vec()); + v.push(i as u8); + DecodedPathSlice(v) + } + + fn with_slice(&self, slice: NibbleSlice) -> DecodedPathSlice { + let mut v = self.0.clone(); + v.append(&mut slice.to_vec()); + DecodedPathSlice(v) + } + + fn with_path_slice(&self, path_slice: &DecodedPathSlice) -> DecodedPathSlice { + let mut v = self.0.clone(); + v.extend(path_slice.0.as_slice()); + DecodedPathSlice(v) + } + + fn encode(&self) -> PathSlice { + let (encoded, _) = NibbleSlice::from_vec(&self.0); + encoded.to_vec() + } +} + +impl std::fmt::Debug for DecodedPathSlice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + let (encoded, _) = NibbleSlice::from_vec(&self.0); + let nibble_slice = NibbleSlice::from_encoded(&encoded); + writeln!(f, "{:?}", nibble_slice) + } +} + +#[derive(Debug)] +struct DepthFirst<T>(T); + +impl<T> PartialOrd for DepthFirst<T> +where + Self: Ord, +{ + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(&other)) + } +} + +impl<T> PartialEq for DepthFirst<T> +where + Self: Ord, +{ + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +impl<T> Eq for DepthFirst<T> where Self: Ord {} + +impl<T> std::ops::Deref for DepthFirst<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::collections::HashMap; + use std::iter::FromIterator; + + use memorydb::MemoryDB; + use primitives::{Bytes, H256}; + use standardmap::{Alphabet, StandardMap, ValueMode}; + + use super::chunk::RawChunk; + use crate::{Trie, TrieDB, TrieDBMut, TrieMut}; + + fn random_insert_and_restore_with_count(count: usize) { + let standard_map = StandardMap { + alphabet: Alphabet::Custom(b"@QWERTYUIOPASDFGHJKLZXCVBNM[/]^_".to_vec()), + min_key: 5, + journal_key: 0, + value_mode: ValueMode::Index, + count, + } + .make_with(&mut H256::new()); + // Unique standard map + let unique_map: HashMap<Bytes, Bytes> = HashMap::from_iter(standard_map.into_iter()); + + let mut root = H256::new(); + let chunks: HashMap<H256, RawChunk> = { + // We will throw out `db` after snapshot. + let mut db = MemoryDB::new(); + let mut trie = TrieDBMut::new(&mut db, &mut root); + for (key, value) in &unique_map { + trie.insert(key, value).unwrap(); + } + + Snapshot::from_hashdb(&db, root).map(|chunk| (chunk.root, chunk.into_raw_chunk())).collect() + }; + dbg!(chunks.len()); + + let mut db = MemoryDB::new(); + let mut recover = Restore::new(root); + while let Some(chunk_root) = recover.next_to_feed() { + let recovered = chunks[&chunk_root].recover(chunk_root).unwrap(); + recover.feed(&mut db, recovered); + } + + let trie = TrieDB::try_new(&db, &root).unwrap(); + for (key, value) in &unique_map { + assert_eq!(trie.get(key).unwrap().as_ref(), Some(value)); + } + } + + #[test] + fn random_insert_and_restore_0() { + random_insert_and_restore_with_count(0); + } + + #[test] + fn random_insert_and_restore_1() { + random_insert_and_restore_with_count(1); + } + + #[test] + fn random_insert_and_restore_2() { + random_insert_and_restore_with_count(2); + } + + #[test] + fn random_insert_and_restore_100() { + random_insert_and_restore_with_count(100); + } + + #[test] + fn random_insert_and_restore_10000() { + random_insert_and_restore_with_count(10_000); + } + + #[test] + #[ignore] + fn random_insert_and_restore_100000() { + random_insert_and_restore_with_count(100_000); + } +} diff --git a/util/merkle/src/snapshot/ordered_heap.rs b/util/merkle/src/snapshot/ordered_heap.rs new file mode 100644 index 0000000000..d83efd77c1 --- /dev/null +++ b/util/merkle/src/snapshot/ordered_heap.rs @@ -0,0 +1,76 @@ +// Copyright 2019 Kodebox, Inc. +// This file is part of CodeChain. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +use std::cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}; +use std::collections::BinaryHeap; + +pub struct OrderedHeap<T> { + heap: BinaryHeap<OrderedHeapEntry<T>>, + seq: usize, +} + +impl<T: Ord> OrderedHeap<T> { + pub fn new() -> OrderedHeap<T> { + OrderedHeap { + heap: BinaryHeap::new(), + seq: 0, + } + } + + pub fn push(&mut self, value: T) { + self.heap.push(OrderedHeapEntry { + seq: self.seq, + value, + }); + self.seq += 1; + } + + pub fn pop(&mut self) -> Option<T> { + self.heap.pop().map(|x| x.value) + } +} + +#[derive(Debug, Clone)] +struct OrderedHeapEntry<T> { + seq: usize, + value: T, +} + +impl<T: Ord> Ord for OrderedHeapEntry<T> { + fn cmp(&self, other: &Self) -> Ordering { + self.value.cmp(&other.value).then(self.seq.cmp(&other.seq).reverse()) + } +} + +impl<T> PartialOrd for OrderedHeapEntry<T> +where + Self: Ord, +{ + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(&other)) + } +} + +impl<T> PartialEq for OrderedHeapEntry<T> +where + Self: Ord, +{ + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +impl<T> Eq for OrderedHeapEntry<T> where Self: Ord {} diff --git a/util/merkle/src/triedb.rs b/util/merkle/src/triedb.rs index d10a4f00ca..a456db9004 100644 --- a/util/merkle/src/triedb.rs +++ b/util/merkle/src/triedb.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. -use ccrypto::blake256; +use ccrypto::{blake256, BLAKE_NULL_RLP}; use hashdb::HashDB; use primitives::H256; @@ -105,6 +105,26 @@ impl<'db> TrieDB<'db> { None => Ok(None), } } + + /// Check if every leaf of the trie exists + pub fn is_complete(&self) -> bool { + *self.root == BLAKE_NULL_RLP || self.is_complete_aux(self.root) + } + + /// Check if every leaf of the trie starting from `hash` exists + pub fn is_complete_aux(&self, hash: &H256) -> bool { + if let Some(node_rlp) = self.db.get(hash) { + match RlpNode::decoded(node_rlp.as_ref()) { + Some(RlpNode::Branch(.., children)) => { + children.iter().flatten().all(|child| self.is_complete_aux(child)) + } + Some(RlpNode::Leaf(..)) => true, + None => false, + } + } else { + false + } + } } impl<'db> Trie for TrieDB<'db> { @@ -126,6 +146,19 @@ mod tests { use crate::*; use memorydb::*; + fn delete_any_child(db: &mut MemoryDB, root: &H256) { + let node_rlp = db.get(root).unwrap(); + match RlpNode::decoded(&node_rlp).unwrap() { + RlpNode::Leaf(..) => { + db.remove(root); + } + RlpNode::Branch(.., children) => { + let first_child = children.iter().find(|c| c.is_some()).unwrap().unwrap(); + db.remove(&first_child); + } + } + } + #[test] fn get() { let mut memdb = MemoryDB::new(); @@ -141,4 +174,33 @@ mod tests { assert_eq!(t.get(b"B"), Ok(Some(b"ABCBA".to_vec()))); assert_eq!(t.get(b"C"), Ok(None)); } + + #[test] + fn is_complete_success() { + let mut memdb = MemoryDB::new(); + let mut root = H256::new(); + { + let mut t = TrieDBMut::new(&mut memdb, &mut root); + t.insert(b"A", b"ABC").unwrap(); + t.insert(b"B", b"ABCBA").unwrap(); + } + + let t = TrieDB::try_new(&memdb, &root).unwrap(); + assert!(t.is_complete()); + } + + #[test] + fn is_complete_fail() { + let mut memdb = MemoryDB::new(); + let mut root = H256::new(); + { + let mut t = TrieDBMut::new(&mut memdb, &mut root); + t.insert(b"A", b"ABC").unwrap(); + t.insert(b"B", b"ABCBA").unwrap(); + } + delete_any_child(&mut memdb, &root); + + let t = TrieDB::try_new(&memdb, &root).unwrap(); + assert!(!t.is_complete()); + } } diff --git a/util/merkle/src/triedbmut.rs b/util/merkle/src/triedbmut.rs index 5684ae0556..6bb89adedc 100644 --- a/util/merkle/src/triedbmut.rs +++ b/util/merkle/src/triedbmut.rs @@ -170,6 +170,113 @@ impl<'a> TrieDBMut<'a> { } } + pub(crate) fn insert_raw(&mut self, node: RlpNode) -> crate::Result<Option<DBValue>> { + let mut old_val = None; + let cur_hash = *self.root; + *self.root = self.insert_raw_aux(node, Some(cur_hash), &mut old_val)?; + + Ok(old_val) + } + + fn insert_raw_aux( + &mut self, + node: RlpNode, + cur_node_hash: Option<H256>, + old_val: &mut Option<DBValue>, + ) -> crate::Result<H256> { + let path = match &node { + RlpNode::Leaf(slice, _) | RlpNode::Branch(slice, _) => slice, + }; + + match cur_node_hash { + Some(hash) => { + let existing_node_rlp = self.db.get(&hash).ok_or_else(|| TrieError::IncompleteDatabase(hash))?; + match RlpNode::decoded(&existing_node_rlp) { + Some(RlpNode::Leaf(partial, value)) => { + // Renew the Leaf + if &partial == path { + let hash = self.db.insert(&RlpNode::encoded(node)); + *old_val = Some(existing_node_rlp); + Ok(hash) + } else { + // Make branch node and insert Leaves + let common = partial.common_prefix(&path); + let mut new_child = empty_children(); + let new_partial = partial.mid(common); + let new_path = path.mid(common); + new_child[new_partial.at(0) as usize] = Some(self.insert_aux( + new_partial.mid(1), + value, + new_child[new_partial.at(0) as usize], + old_val, + )?); + new_child[new_path.at(0) as usize] = Some(self.insert_raw_aux( + node.mid(common + 1), + new_child[new_path.at(0) as usize], + old_val, + )?); + + let hash = self + .db + .insert(&RlpNode::encoded_until(RlpNode::Branch(partial, new_child.into()), common)); + + Ok(hash) + } + } + Some(RlpNode::Branch(partial, mut children)) => { + let common = partial.common_prefix(&path); + + // Make new branch node and insert leaf and branch with new path + if common < partial.len() { + let mut new_child = empty_children(); + let new_partial = partial.mid(common); + let new_path = path.mid(common); + let o_branch = RlpNode::Branch(new_partial.mid(1), children); + + let b_hash = self.db.insert(&RlpNode::encoded(o_branch)); + + new_child[new_partial.at(0) as usize] = Some(b_hash); + new_child[new_path.at(0) as usize] = Some(self.insert_raw_aux( + node.mid(common + 1), + new_child[new_path.at(0) as usize], + old_val, + )?); + + let hash = self + .db + .insert(&RlpNode::encoded_until(RlpNode::Branch(partial, new_child.into()), common)); + + Ok(hash) + } else { + // Insert leaf into the branch node + let new_path = path.mid(common); + + children[new_path.at(0) as usize] = Some(self.insert_raw_aux( + node.mid(common + 1), + children[new_path.at(0) as usize], + old_val, + )?); + + let new_branch = RlpNode::Branch(partial, children); + let node_rlp = RlpNode::encoded(new_branch); + let hash = self.db.insert(&node_rlp); + + Ok(hash) + } + } + None => { + let hash = self.db.insert(&RlpNode::encoded(node)); + Ok(hash) + } + } + } + None => { + let hash = self.db.insert(&RlpNode::encoded(node)); + Ok(hash) + } + } + } + /// Remove auxiliary fn remove_aux( &mut self,