diff --git a/bin/light-base/src/json_rpc_service.rs b/bin/light-base/src/json_rpc_service.rs index ea0cf62060..3c4475c448 100644 --- a/bin/light-base/src/json_rpc_service.rs +++ b/bin/light-base/src/json_rpc_service.rs @@ -346,7 +346,7 @@ impl ServicePrototype { Default::default(), ), }), - genesis_block: config.genesis_block_hash, + genesis_block_hash: config.genesis_block_hash, next_subscription_id: atomic::AtomicU64::new(0), subscriptions: Mutex::new(Subscriptions { misc: HashMap::with_capacity_and_hasher( @@ -466,7 +466,7 @@ struct Background { /// Hash of the genesis block. /// Keeping the genesis block is important, as the genesis block hash is included in /// transaction signatures, and must therefore be queried by upper-level UIs. - genesis_block: [u8; 32], + genesis_block_hash: [u8; 32], /// Identifier to use for the next subscription. /// diff --git a/bin/light-base/src/json_rpc_service/chain_head.rs b/bin/light-base/src/json_rpc_service/chain_head.rs index 0e1754908d..d97779ac34 100644 --- a/bin/light-base/src/json_rpc_service/chain_head.rs +++ b/bin/light-base/src/json_rpc_service/chain_head.rs @@ -1441,6 +1441,7 @@ impl Background { let response = super::super::encode_database( &self.network_service.0, &self.sync_service, + &self.genesis_block_hash, self.sync_service.block_number_bytes(), usize::try_from(max_size_bytes.unwrap_or(u64::max_value())) .unwrap_or(usize::max_value()), diff --git a/bin/light-base/src/json_rpc_service/getters.rs b/bin/light-base/src/json_rpc_service/getters.rs index 47aaf08ad1..0f2ed8d051 100644 --- a/bin/light-base/src/json_rpc_service/getters.rs +++ b/bin/light-base/src/json_rpc_service/getters.rs @@ -61,7 +61,7 @@ impl Background { .respond( state_machine_request_id, methods::Response::chainHead_unstable_genesisHash(methods::HashHexString( - self.genesis_block, + self.genesis_block_hash, )) .to_json_response(request_id), ) @@ -93,7 +93,7 @@ impl Background { .respond( state_machine_request_id, methods::Response::chainSpec_unstable_genesisHash(methods::HashHexString( - self.genesis_block, + self.genesis_block_hash, )) .to_json_response(request_id), ) diff --git a/bin/light-base/src/json_rpc_service/state_chain.rs b/bin/light-base/src/json_rpc_service/state_chain.rs index d8008f72fc..25f9d488d5 100644 --- a/bin/light-base/src/json_rpc_service/state_chain.rs +++ b/bin/light-base/src/json_rpc_service/state_chain.rs @@ -211,7 +211,7 @@ impl Background { let response = { match height { Some(0) => methods::Response::chain_getBlockHash(methods::HashHexString( - self.genesis_block, + self.genesis_block_hash, )) .to_json_response(request_id), None => { diff --git a/bin/light-base/src/lib.rs b/bin/light-base/src/lib.rs index 3145cf28d5..ab0a1432f7 100644 --- a/bin/light-base/src/lib.rs +++ b/bin/light-base/src/lib.rs @@ -369,8 +369,16 @@ impl Client { ), ) { // Use the database if it contains a more recent block than the chain spec checkpoint. - (Ok(Ok(genesis_ci)), checkpoint, Ok((database, checkpoint_nodes))) - if checkpoint + ( + Ok(Ok(genesis_ci)), + checkpoint, + Ok((genesis_hash, database, checkpoint_nodes)), + ) if genesis_hash + == genesis_ci + .as_ref() + .finalized_block_header + .hash(chain_spec.block_number_bytes().into()) + && checkpoint .as_ref() .and_then(|r| r.as_ref().ok()) .map_or(true, |cp| { @@ -386,7 +394,7 @@ impl Client { ( Err(chain_spec::FromGenesisStorageError::UnknownStorageItems), checkpoint, - Ok((database, checkpoint_nodes)), + Ok((database_genesis_hash, database, checkpoint_nodes)), ) if checkpoint .as_ref() .and_then(|r| r.as_ref().ok()) @@ -403,7 +411,20 @@ impl Client { digest: header::DigestRef::empty().into(), }; - (database, genesis_header, checkpoint_nodes) + if database_genesis_hash + == genesis_header.hash(chain_spec.block_number_bytes().into()) + { + (database, genesis_header, checkpoint_nodes) + } else if let Some(Ok(checkpoint)) = checkpoint { + // Database is incorrect. + (checkpoint, genesis_header, checkpoint_nodes) + } else { + // TODO: we can in theory support chain specs that have neither a checkpoint nor the genesis storage, but it's complicated + return Err( + "Either a checkpoint or the genesis storage must be provided" + .to_string(), + ); + } } (Err(chain_spec::FromGenesisStorageError::UnknownStorageItems), None, _) => { @@ -1115,11 +1136,13 @@ async fn start_services( async fn encode_database( network_service: &network_service::NetworkService, sync_service: &sync_service::SyncService, + genesis_block_hash: &[u8; 32], block_number_bytes: usize, max_size: usize, ) -> String { // Craft the structure containing all the data that we would like to include. let mut database_draft = SerdeDatabase { + genesis_hash: hex::encode(genesis_block_hash), chain: match sync_service.serialize_chain_information().await { Some(ci) => { let encoded = finalized_serialize::encode_chain(&ci, block_number_bytes); @@ -1188,6 +1211,7 @@ fn decode_database( block_number_bytes: usize, ) -> Result< ( + [u8; 32], chain::chain_information::ValidChainInformation, Vec<(PeerId, Vec)>, ), @@ -1195,6 +1219,12 @@ fn decode_database( > { let decoded: SerdeDatabase = serde_json::from_str(encoded).map_err(|_| ())?; + let genesis_hash = if decoded.genesis_hash.len() == 64 { + <[u8; 32]>::try_from(hex::decode(&decoded.genesis_hash).map_err(|_| ())?).unwrap() + } else { + return Err(()); + }; + let (chain, _) = finalized_serialize::decode_chain( &serde_json::to_string(&decoded.chain).unwrap(), block_number_bytes, @@ -1216,11 +1246,14 @@ fn decode_database( }) .collect::>(); - Ok((chain, nodes)) + Ok((genesis_hash, chain, nodes)) } #[derive(serde::Serialize, serde::Deserialize)] struct SerdeDatabase { + /// Hexadecimal-encoded hash of the genesis block header. Has no `0x` prefix. + #[serde(rename = "genesisHash")] + genesis_hash: String, chain: Box, nodes: hashbrown::HashMap, fnv::FnvBuildHasher>, } diff --git a/bin/wasm-node/CHANGELOG.md b/bin/wasm-node/CHANGELOG.md index f3e6849861..c06c69dbe6 100644 --- a/bin/wasm-node/CHANGELOG.md +++ b/bin/wasm-node/CHANGELOG.md @@ -6,6 +6,7 @@ - The `payment_queryInfo` JSON-RPC function now works with runtimes that have defined the type of `Balance` to be less than 16 bytes. ([#2914](https://github.com/paritytech/smoldot/pull/2914)) - The parameter of `chainHead_unstable_finalizedDatabase` has been renamed from `max_size_bytes` to `maxSizeBytes`. ([#2923](https://github.com/paritytech/smoldot/pull/2923)) +- The database now contains the hash of the genesis block header. This hash is verified when the database is loaded, and the database is ignored if there is a mismatch. This prevents accidents where the wrong database is provided, which would lead to the chain not working and would be hard to debug. ([#2928](https://github.com/paritytech/smoldot/pull/2928)) ## 0.7.3 - 2022-10-19