From e68b0472da26f62193bf2920cfece3b63ebc1cca Mon Sep 17 00:00:00 2001 From: Pierre Krieger Date: Thu, 6 Jul 2023 10:38:14 +0200 Subject: [PATCH 1/5] Put the code storage and merkle value in the database --- Cargo.lock | 1 + lib/src/sync/all.rs | 21 +++- lib/src/sync/warp_sync.rs | 96 ++++++++++++++++- light-base/Cargo.toml | 1 + light-base/src/database.rs | 52 ++++++++- light-base/src/json_rpc_service/background.rs | 26 ++++- .../json_rpc_service/background/getters.rs | 1 + light-base/src/lib.rs | 44 ++++++-- light-base/src/runtime_service.rs | 63 +++++++++-- light-base/src/sync_service.rs | 102 ++++++++++++------ light-base/src/sync_service/standalone.rs | 12 ++- 11 files changed, 357 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 09d7ae9fcb..488c270ec8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2477,6 +2477,7 @@ version = "0.6.0" dependencies = [ "async-channel", "async-lock", + "base64 0.21.2", "blake2-rfc", "derive_more", "either", diff --git a/lib/src/sync/all.rs b/lib/src/sync/all.rs index 958578969b..a06f87353a 100644 --- a/lib/src/sync/all.rs +++ b/lib/src/sync/all.rs @@ -47,7 +47,9 @@ use core::{ }; pub use crate::executor::vm::ExecHint; -pub use warp_sync::{FragmentError as WarpSyncFragmentError, WarpSyncFragment}; +pub use warp_sync::{ + ConfigCodeTrieNodeHint, FragmentError as WarpSyncFragmentError, WarpSyncFragment, +}; /// Configuration for the [`AllSync`]. // TODO: review these fields @@ -109,6 +111,15 @@ pub struct Config { /// verified. // TODO: change this now that we don't verify block bodies here pub full_mode: bool, + + /// Known valid Merkle value and storage value combination for the `:code` key. + /// + /// If provided, the warp syncing algorithm will first fetch the Merkle value of `:code`, and + /// if it matches the Merkle value provided in the hint, use the storage value in the hint + /// instead of downloading it. If the hint doesn't match, an extra round-trip will be needed, + /// but if the hint matches it saves a big download. + // TODO: provide only in non-full mode? + pub code_trie_node_hint: Option, } /// Identifier for a source in the [`AllSync`]. @@ -185,6 +196,7 @@ impl AllSync { block_number_bytes: config.block_number_bytes, sources_capacity: config.sources_capacity, requests_capacity: config.sources_capacity, // TODO: ?! add as config? + code_trie_node_hint: config.code_trie_node_hint, }) { Ok(inner) => AllSyncInner::GrandpaWarpSync { inner: warp_sync::WarpSync::InProgress(inner), @@ -1306,6 +1318,7 @@ impl AllSync { finalized_block_runtime, finalized_storage_code, finalized_storage_heap_pages, + finalized_storage_code_merkle_value, ) = self.shared.transition_grandpa_warp_sync_all_forks(success); self.inner = AllSyncInner::AllForks(new_inner); ProcessOne::WarpSyncFinished { @@ -1313,6 +1326,7 @@ impl AllSync { finalized_block_runtime, finalized_storage_code, finalized_storage_heap_pages, + finalized_storage_code_merkle_value, } } AllSyncInner::AllForks(sync) => match sync.process_one() { @@ -2299,6 +2313,9 @@ pub enum ProcessOne { /// Storage value at the `:heappages` key of the finalized block. finalized_storage_heap_pages: Option>, + + /// Merkle value of the `:code` trie node of the finalized block. + finalized_storage_code_merkle_value: Option>, }, /// Ready to start verifying a block. @@ -2970,6 +2987,7 @@ impl Shared { host::HostVmPrototype, Option>, Option>, + Option>, ) { let mut all_forks = all_forks::AllForksSync::new(all_forks::Config { chain_information: grandpa.chain_information, @@ -3073,6 +3091,7 @@ impl Shared { grandpa.finalized_runtime, grandpa.finalized_storage_code, grandpa.finalized_storage_heap_pages, + grandpa.finalized_storage_code_merkle_value, ) } } diff --git a/lib/src/sync/warp_sync.rs b/lib/src/sync/warp_sync.rs index 2eddfcd180..b3e3ba4ed2 100644 --- a/lib/src/sync/warp_sync.rs +++ b/lib/src/sync/warp_sync.rs @@ -102,7 +102,7 @@ use crate::{ vm::ExecHint, }, header::{self, Header}, - trie::proof_decode, + trie::{self, proof_decode}, }; use alloc::{ @@ -143,6 +143,7 @@ pub enum Error { } /// The configuration for [`start_warp_sync()`]. +#[derive(Debug)] pub struct Config { /// The chain information of the starting point of the warp syncing. pub start_chain_information: ValidChainInformation, @@ -156,6 +157,24 @@ pub struct Config { /// The initial capacity of the list of requests. pub requests_capacity: usize, + + /// Known valid Merkle value and storage value combination for the `:code` key. + /// + /// If provided, the warp syncing algorithm will first fetch the Merkle value of `:code`, and + /// if it matches the Merkle value provided in the hint, use the storage value in the hint + /// instead of downloading it. If the hint doesn't match, an extra round-trip will be needed, + /// but if the hint matches it saves a big download. + pub code_trie_node_hint: Option, +} + +/// See [`Config::code_trie_node_hint`]. +#[derive(Debug)] +pub struct ConfigCodeTrieNodeHint { + /// Potential Merkle value of the `:code` key. + pub merkle_value: Vec, + + /// Storage value corresponding to [`ConfigCodeTrieNodeHint::merkle_value`]. + pub storage_value: Vec, } /// Initializes the warp sync state machine. @@ -186,6 +205,7 @@ pub fn start_warp_sync( Ok(InProgressWarpSync { start_chain_information: config.start_chain_information, + code_trie_node_hint: config.code_trie_node_hint, block_number_bytes: config.block_number_bytes, sources: slab::Slab::with_capacity(config.sources_capacity), in_progress_requests: slab::Slab::with_capacity(config.requests_capacity), @@ -236,6 +256,9 @@ pub struct Success { /// Storage value at the `:heappages` key of the finalized block. pub finalized_storage_heap_pages: Option>, + /// Merkle value of the `:code` trie node of the finalized block. + pub finalized_storage_code_merkle_value: Option>, + /// The list of sources that were added to the state machine. /// The list is ordered by [`SourceId`]. pub sources_ordered: Vec<(SourceId, TSrc)>, @@ -275,6 +298,8 @@ impl ops::IndexMut for InProgressWarpSync { pub struct InProgressWarpSync { /// See [`Phase`]. phase: Phase, + /// See [`Config::code_trie_node_hint`]. + code_trie_node_hint: Option, /// Starting point of the warp syncing, as provided to [`start_warp_sync`]. start_chain_information: ValidChainInformation, /// Number of bytes used to encode the block number in headers. @@ -317,6 +342,9 @@ enum Phase { /// Source we downloaded the last fragments from. Assuming that the source isn't malicious, /// it is guaranteed to have access to the storage of the finalized block. warp_sync_source_id: SourceId, + /// `true` if it is known that [`InProgressWarpSync::code_trie_node_hint`] doesn't match + /// the storage of the header we warp synced to. + hint_doesnt_match: bool, /// Merkle proof containing the runtime information, or `None` if it was not downloaded yet. downloaded_runtime: Option>, }, @@ -353,6 +381,8 @@ struct DownloadedRuntime { storage_code: Option>, /// Storage item at the `:heappages` key. `None` if there is no entry at that key. storage_heap_pages: Option>, + /// Merkle value of the `:code` trie node. `None` if there is no entry at that key. + code_merkle_value: Option>, } /// See [`InProgressWarpSync::status`]. @@ -620,9 +650,17 @@ impl InProgressWarpSync { let runtime_parameters_get = if let Phase::RuntimeDownload { header, warp_sync_source_id, + hint_doesnt_match, .. } = &self.phase { + let code_key_to_request = if !*hint_doesnt_match && self.code_trie_node_hint.is_some() { + // TODO: this is actually missing a nibble in order to be sure that the Merkle value of `:code` will be found in the proof, but given that this whole mechanism will be replaced with something else in the future, I don't want to make tons of API changes to support nibbles here + &b":cod"[..] + } else { + &b":code"[..] + }; + // TODO: O(n) if !self.in_progress_requests.iter().any(|(_, rq)| { rq.0 == *warp_sync_source_id @@ -631,7 +669,7 @@ impl InProgressWarpSync { block_hash: ref b, ref keys, } if *b == header.hash(self.block_number_bytes) - && keys.iter().any(|k| k == b":code") + && keys.iter().any(|k| k == code_key_to_request) && keys.iter().any(|k| k == b":heappages")) }) { Some(( @@ -640,7 +678,7 @@ impl InProgressWarpSync { DesiredRequest::StorageGetMerkleProof { block_hash: header.hash(self.block_number_bytes), state_trie_root: header.state_root, - keys: vec![b":code".to_vec(), b":heappages".to_vec()], + keys: vec![code_key_to_request.to_vec(), b":heappages".to_vec()], }, )) } else { @@ -1133,6 +1171,7 @@ impl VerifyWarpSyncFragment { .into(), warp_sync_source_id: *downloaded_source, downloaded_runtime: None, + hint_doesnt_match: false, }; } Ok(verifier::Next::Success { @@ -1152,6 +1191,7 @@ impl VerifyWarpSyncFragment { chain_information_finality, warp_sync_source_id: *downloaded_source, downloaded_runtime: None, + hint_doesnt_match: false, }; } else { self.inner.phase = Phase::DownloadFragments { @@ -1198,6 +1238,7 @@ impl BuildRuntime { downloaded_runtime, chain_information_finality, warp_sync_source_id, + hint_doesnt_match, .. } = &mut self.inner.phase { @@ -1221,7 +1262,45 @@ impl BuildRuntime { } }; - let finalized_storage_code = + let finalized_storage_code_merkle_value = match decoded_downloaded_runtime + .closest_descendant_merkle_value( + &header.state_root, + &trie::bytes_to_nibbles(b":code".iter().copied()).collect::>(), + ) { + Ok(Some(merkle_value)) => merkle_value.to_owned(), + Ok(None) => { + self.inner.phase = Phase::DownloadFragments { + previous_verifier_values: Some(( + header.clone(), + chain_information_finality.clone(), + )), + }; + return (WarpSync::InProgress(self.inner), Some(Error::MissingCode)); + } + Err(proof_decode::IncompleteProofError { .. }) => { + self.inner.phase = Phase::DownloadFragments { + previous_verifier_values: Some(( + header.clone(), + chain_information_finality.clone(), + )), + }; + return ( + WarpSync::InProgress(self.inner), + Some(Error::MerkleProofEntriesMissing), + ); + } + }; + + let finalized_storage_code = if let (false, Some(hint)) = + (*hint_doesnt_match, self.inner.code_trie_node_hint.as_ref()) + { + if hint.merkle_value == finalized_storage_code_merkle_value { + &hint.storage_value + } else { + *hint_doesnt_match = true; + return (WarpSync::InProgress(self.inner), None); + } + } else { match decoded_downloaded_runtime.storage_value(&header.state_root, b":code") { Ok(Some((code, _))) => code, Ok(None) => { @@ -1245,7 +1324,8 @@ impl BuildRuntime { Some(Error::MerkleProofEntriesMissing), ); } - }; + } + }; let finalized_storage_heappages = match decoded_downloaded_runtime.storage_value(&header.state_root, b":heappages") { @@ -1332,6 +1412,9 @@ impl BuildRuntime { finalized_storage_code: Some(finalized_storage_code.to_owned()), finalized_storage_heap_pages: finalized_storage_heappages .map(|v| v.to_vec()), + finalized_storage_code_merkle_value: Some( + finalized_storage_code_merkle_value, + ), sources_ordered: mem::take(&mut self.inner.sources) .into_iter() .map(|(id, source)| (SourceId(id), source.user_data)) @@ -1377,6 +1460,7 @@ impl BuildRuntime { downloaded_runtime: Some(DownloadedRuntime { storage_code: Some(finalized_storage_code.to_vec()), storage_heap_pages: finalized_storage_heappages.map(|v| v.to_vec()), + code_merkle_value: Some(finalized_storage_code_merkle_value), }), chain_info_builder: Some(chain_info_builder), calls, @@ -1482,6 +1566,8 @@ impl BuildChainInformation { finalized_storage_code: downloaded_runtime.storage_code, finalized_storage_heap_pages: downloaded_runtime .storage_heap_pages, + finalized_storage_code_merkle_value: downloaded_runtime + .code_merkle_value, sources_ordered: mem::take(&mut self.inner.sources) .into_iter() .map(|(id, source)| (SourceId(id), source.user_data)) diff --git a/light-base/Cargo.toml b/light-base/Cargo.toml index d8d1c66943..08f3a2c42b 100644 --- a/light-base/Cargo.toml +++ b/light-base/Cargo.toml @@ -14,6 +14,7 @@ required-features = ["std"] [dependencies] async-channel = { version = "1.8.0", default-features = false } # TODO: no-std-ize; this is has been done and is just waiting for a release: https://github.com/smol-rs/event-listener/pull/34 async-lock = { version = "2.7.0", default-features = false } # TODO: no-std-ize; this is has been done and is just waiting for a release: https://github.com/smol-rs/event-listener/pull/34 +base64 = { version = "0.21.2", default-features = false, features = ["alloc"] } blake2-rfc = { version = "0.2.18", default-features = false } derive_more = "0.99.17" either = { version = "1.8.1", default-features = false } diff --git a/light-base/src/database.rs b/light-base/src/database.rs index 8150392322..7744307b9e 100644 --- a/light-base/src/database.rs +++ b/light-base/src/database.rs @@ -40,7 +40,7 @@ use smoldot::{ libp2p::{multiaddr, PeerId}, }; -use crate::{network_service, platform, sync_service}; +use crate::{network_service, platform, runtime_service, sync_service}; /// A decoded database. pub struct DatabaseContent { @@ -51,6 +51,20 @@ pub struct DatabaseContent { /// List of nodes that were known to be part of the peer-to-peer network when the database /// was encoded. pub known_nodes: Vec<(PeerId, Vec)>, + /// Known valid Merkle value and storage value combination for the `:code` key. + /// + /// Does **not** necessarily match the finalized block found in + /// [`DatabaseCOntent::chain_information`]. + pub runtime_code_hint: Option, +} + +/// See [`DatabaseContent::runtime_code_hint`]. +pub struct DatabaseContentRuntimeCodeHint { + /// Storage value of the `:code` trie node corresponding to + /// [`DatabaseContentRuntimeCodeHint::code_merkle_value`]. + pub code: Vec, + /// Merkle value of the `:code` trie node in the storage main trie. + pub code_merkle_value: Vec, } /// Serializes the finalized state of the chain, using the given services. @@ -60,9 +74,15 @@ pub struct DatabaseContent { pub async fn encode_database( network_service: &network_service::NetworkService, sync_service: &sync_service::SyncService, + runtime_service: &runtime_service::RuntimeService, genesis_block_hash: &[u8; 32], max_size: usize, ) -> String { + let (code_storage_value, code_merkle_value) = runtime_service + .finalized_runtime_storage_merkle_values() + .await + .unwrap_or((None, None)); + // 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), @@ -93,6 +113,12 @@ pub async fn encode_database( ) }) .collect(), + code_merkle_value: code_merkle_value.map(hex::encode), + // While it might seem like a good idea to compress the runtime code, in practice it is + // normally already zstd-compressed, and additional compressing shouldn't improve the size. + code_storage_value: code_storage_value.map(|data| { + base64::Engine::encode(&base64::engine::general_purpose::STANDARD_NO_PAD, data) + }), }; // Cap the database length to the maximum size. @@ -103,6 +129,14 @@ pub async fn encode_database( return serialized; } + // Scrap the code, as it is the biggest item. + if database_draft.code_merkle_value.is_some() || database_draft.code_storage_value.is_some() + { + database_draft.code_merkle_value = None; + database_draft.code_storage_value = None; + continue; + } + if database_draft.nodes.is_empty() { // Can't shrink the database anymore. Return the string `""` which will // fail to decode but will indicate what is wrong. @@ -167,10 +201,22 @@ pub fn decode_database(encoded: &str, block_number_bytes: usize) -> Result>(); + let runtime_code_hint = match (decoded.code_merkle_value, decoded.code_storage_value) { + (Some(mv), Some(sv)) => Some(DatabaseContentRuntimeCodeHint { + code: base64::Engine::decode(&base64::engine::general_purpose::STANDARD_NO_PAD, &sv) + .map_err(|_| ())?, + code_merkle_value: hex::decode(&mv).map_err(|_| ())?, + }), + // A combination of `Some` and `None` is technically invalid, but we simply ignore this + // situation. + _ => None, + }; + Ok(DatabaseContent { genesis_block_hash, chain_information, known_nodes, + runtime_code_hint, }) } @@ -181,4 +227,8 @@ struct SerdeDatabase { genesis_hash: String, chain: Box, nodes: hashbrown::HashMap, fnv::FnvBuildHasher>, + #[serde(skip_serializing_if = "Option::is_none")] + code_storage_value: Option, + #[serde(skip_serializing_if = "Option::is_none")] + code_merkle_value: Option, } diff --git a/light-base/src/json_rpc_service/background.rs b/light-base/src/json_rpc_service/background.rs index b97e95847d..3409c2ad7e 100644 --- a/light-base/src/json_rpc_service/background.rs +++ b/light-base/src/json_rpc_service/background.rs @@ -832,7 +832,7 @@ impl Background { // Download the runtime of this block. This takes a long time as the runtime is rather // big (around 1MiB in general). - let (storage_code, storage_heap_pages) = { + let (storage_code, storage_heap_pages, code_merkle_value) = { let entries = self .sync_service .clone() @@ -841,6 +841,10 @@ impl Background { block_hash, &state_trie_root_hash, [ + sync_service::StorageRequestItem { + key: b":code".to_vec(), + ty: sync_service::StorageRequestItemTy::ClosestDescendantMerkleValue, + }, sync_service::StorageRequestItem { key: b":code".to_vec(), ty: sync_service::StorageRequestItemTy::Value, @@ -879,14 +883,30 @@ impl Background { _ => None, }) .unwrap(); - (code, heap_pages) + let code_merkle_value = if code.is_some() { + entries + .iter() + .find_map(|entry| match entry { + sync_service::StorageResultItem::ClosestDescendantMerkleValue { + requested_key, + closest_descendant_merkle_value, + } if requested_key == b":code" => { + Some(closest_descendant_merkle_value.clone()) // TODO overhead + } + _ => None, + }) + .unwrap() + } else { + None + }; + (code, heap_pages, code_merkle_value) }; // Give the code and heap pages to the runtime service. The runtime service will // try to find any similar runtime it might have, and if not will compile it. let pinned_runtime_id = self .runtime_service - .compile_and_pin_runtime(storage_code, storage_heap_pages) + .compile_and_pin_runtime(storage_code, storage_heap_pages, code_merkle_value) .await; let precall = self diff --git a/light-base/src/json_rpc_service/background/getters.rs b/light-base/src/json_rpc_service/background/getters.rs index e6f16c2dae..babc4767a3 100644 --- a/light-base/src/json_rpc_service/background/getters.rs +++ b/light-base/src/json_rpc_service/background/getters.rs @@ -202,6 +202,7 @@ impl Background { let response = crate::database::encode_database( &self.network_service.0, &self.sync_service, + &self.runtime_service, &self.genesis_block_hash, usize::try_from(max_size_bytes.unwrap_or(u64::max_value())) .unwrap_or(usize::max_value()), diff --git a/light-base/src/lib.rs b/light-base/src/lib.rs index b66df72fad..9aee104864 100644 --- a/light-base/src/lib.rs +++ b/light-base/src/lib.rs @@ -368,7 +368,7 @@ impl Client { // known as a checkpoint) is present in the chain spec, it is possible to start syncing at // the finalized block it describes. // TODO: clean up that block - let (chain_information, genesis_block_header, checkpoint_nodes) = { + let (chain_information, genesis_block_header, checkpoint_nodes, runtime_code_hint) = { match ( chain_spec.to_chain_information().map(|(ci, _)| ci), // TODO: don't just throw away the runtime chain_spec @@ -403,6 +403,7 @@ impl Client { database_content.chain_information, genesis_header.into(), database_content.known_nodes, + database_content.runtime_code_hint, ) } @@ -438,10 +439,16 @@ impl Client { database_content.chain_information, genesis_header, database_content.known_nodes, + database_content.runtime_code_hint, ) } else if let Some(Ok(checkpoint)) = checkpoint { // Database is incorrect. - (checkpoint, genesis_header, database_content.known_nodes) + ( + checkpoint, + genesis_header, + database_content.known_nodes, + None, + ) } else { // TODO: we can in theory support chain specs that have neither a checkpoint nor the genesis storage, but it's complicated // TODO: is this relevant for parachains? @@ -468,14 +475,14 @@ impl Client { digest: header::DigestRef::empty().into(), }; - (checkpoint, genesis_header, Default::default()) + (checkpoint, genesis_header, Default::default(), None) } (Err(err), _, _) => return Err(AddChainError::InvalidGenesisStorage(err)), (Ok(genesis_ci), Some(Ok(checkpoint)), _) => { let genesis_header = genesis_ci.as_ref().finalized_block_header.clone(); - (checkpoint, genesis_header.into(), Default::default()) + (checkpoint, genesis_header.into(), Default::default(), None) } ( @@ -488,7 +495,7 @@ impl Client { ) => { let genesis_header = header::Header::from(genesis_ci.as_ref().finalized_block_header.clone()); - (genesis_ci, genesis_header, Default::default()) + (genesis_ci, genesis_header, Default::default(), None) } (_, Some(Err(err)), _) => { @@ -705,6 +712,7 @@ impl Client { log_name.clone(), &platform, chain_information, + runtime_code_hint, genesis_block_header .scale_encoding_vec(chain_spec.block_number_bytes().into()), chain_spec, @@ -1060,6 +1068,7 @@ async fn start_services( log_name: String, platform: &TPlat, chain_information: chain::chain_information::ValidChainInformation, + runtime_code_hint: Option, genesis_block_scale_encoded_header: Vec, chain_spec: chain_spec::ChainSpec, relay_chain: Option<&ChainServices>, @@ -1115,11 +1124,15 @@ async fn start_services( block_number_bytes: usize::from(chain_spec.block_number_bytes()), network_service: (network_service.clone(), 0), network_events_receiver: network_event_receivers.pop().unwrap(), - parachain: Some(sync_service::ConfigParachain { - parachain_id: chain_spec.relay_chain().unwrap().1, - relay_chain_sync: relay_chain.runtime_service.clone(), - relay_chain_block_number_bytes: relay_chain.sync_service.block_number_bytes(), - }), + chain_type: sync_service::ConfigChainType::Parachain( + sync_service::ConfigParachain { + parachain_id: chain_spec.relay_chain().unwrap().1, + relay_chain_sync: relay_chain.runtime_service.clone(), + relay_chain_block_number_bytes: relay_chain + .sync_service + .block_number_bytes(), + }, + ), }) .await, ); @@ -1151,7 +1164,16 @@ async fn start_services( platform: platform.clone(), network_service: (network_service.clone(), 0), network_events_receiver: network_event_receivers.pop().unwrap(), - parachain: None, + chain_type: sync_service::ConfigChainType::RelayChain( + sync_service::ConfigRelayChain { + runtime_code_hint: runtime_code_hint.map(|hint| { + sync_service::ConfigRelayChainRuntimeCodeHint { + storage_value: hint.code, + merkle_value: hint.code_merkle_value, + } + }), + }, + ), }) .await, ); diff --git a/light-base/src/runtime_service.rs b/light-base/src/runtime_service.rs index d2745bebe5..1a2591da78 100644 --- a/light-base/src/runtime_service.rs +++ b/light-base/src/runtime_service.rs @@ -420,6 +420,26 @@ impl RuntimeService { } } + /// Returns the storage value and Merkle value of the `:code` key of the finalized block. + /// + /// Returns `None` if the runtime of the current finalized block is not known yet. + pub async fn finalized_runtime_storage_merkle_values( + &self, + ) -> Option<(Option>, Option>)> { + let mut guarded = self.guarded.lock().await; + let guarded = &mut *guarded; + + if let GuardedInner::FinalizedBlockRuntimeKnown { tree, .. } = &guarded.tree { + let runtime = &tree.output_finalized_async_user_data(); + Some(( + runtime.runtime_code.clone(), + runtime.code_merkle_value.clone(), + )) + } else { + None + } + } + /// Lock the runtime service and prepare a call to a runtime entry point. /// /// The hash of the block passed as parameter corresponds to the block whose runtime to use @@ -513,6 +533,7 @@ impl RuntimeService { &self, storage_code: Option>, storage_heap_pages: Option>, + code_merkle_value: Option>, ) -> PinnedRuntimeId { let mut guarded = self.guarded.lock().await; @@ -532,6 +553,7 @@ impl RuntimeService { let runtime = Arc::new(Runtime { heap_pages: storage_heap_pages, runtime_code: storage_code, + code_merkle_value, runtime, }); guarded.runtimes.insert(Arc::downgrade(&runtime)); @@ -1211,6 +1233,7 @@ async fn run_background( let runtime = Arc::new(Runtime { runtime_code: finalized_block_runtime.storage_code, heap_pages: finalized_block_runtime.storage_heap_pages, + code_merkle_value: finalized_block_runtime.code_merkle_value, runtime: Ok(SuccessfulRuntime { runtime_spec: finalized_block_runtime .virtual_machine @@ -1485,7 +1508,7 @@ async fn run_background( }.format_with(", ", |block, fmt| fmt(&HashDisplay(&block.hash))).to_string(); match download_result { - Ok((storage_code, storage_heap_pages)) => { + Ok((storage_code, storage_heap_pages, code_merkle_value)) => { log::debug!( target: &log_target, "Worker <= SuccessfulDownload(blocks=[{}])", @@ -1496,7 +1519,7 @@ async fn run_background( guarded.best_near_head_of_chain = true; drop(guarded); - background.runtime_download_finished(async_op_id, storage_code, storage_heap_pages).await; + background.runtime_download_finished(async_op_id, storage_code, storage_heap_pages, code_merkle_value).await; } Err(error) => { log::debug!( @@ -1569,14 +1592,14 @@ struct Background { blocks_stream: Pin + Send>>, /// List of runtimes currently being downloaded from the network. - /// For each item, the download id, storage value of `:code`, and storage value of - /// `:heappages`. + /// For each item, the download id, storage value of `:code`, storage value of `:heappages`, + /// and Merkle value of `:code`. runtime_downloads: stream::FuturesUnordered< future::BoxFuture< 'static, ( async_tree::AsyncOpId, - Result<(Option>, Option>), RuntimeDownloadError>, + Result<(Option>, Option>, Option>), RuntimeDownloadError>, ), >, >, @@ -1592,6 +1615,7 @@ impl Background { async_op_id: async_tree::AsyncOpId, storage_code: Option>, storage_heap_pages: Option>, + code_merkle_value: Option>, ) { let mut guarded = self.guarded.lock().await; @@ -1634,6 +1658,7 @@ impl Background { heap_pages: storage_heap_pages, runtime_code: storage_code, runtime, + code_merkle_value, }); guarded.runtimes.insert(Arc::downgrade(&runtime)); @@ -1966,6 +1991,10 @@ impl Background { &block_hash, &state_root, [ + sync_service::StorageRequestItem { + key: b":code".to_vec(), + ty: sync_service::StorageRequestItemTy::ClosestDescendantMerkleValue, + }, sync_service::StorageRequestItem { key: b":code".to_vec(), ty: sync_service::StorageRequestItemTy::Value, @@ -2008,7 +2037,23 @@ impl Background { _ => None, }) .unwrap(); - Ok((code, heap_pages)) + let code_merkle_value = if code.is_some() { + entries + .iter() + .find_map(|entry| match entry { + sync_service::StorageResultItem::ClosestDescendantMerkleValue { + requested_key, + closest_descendant_merkle_value, + } if requested_key == b":code" => { + Some(closest_descendant_merkle_value.clone()) // TODO overhead + } + _ => None, + }) + .unwrap() + } else { + None + }; + Ok((code, heap_pages, code_merkle_value)) } Err(error) => Err(RuntimeDownloadError::StorageQuery(error)), }; @@ -2083,6 +2128,12 @@ struct Runtime { /// happened, including a problem when obtaining the runtime specs. runtime: Result, + /// Merkle value of the `:code` trie node. + /// + /// Can be `None` if the storage is empty, in which case the runtime will have failed to + /// build. + code_merkle_value: Option>, + /// Undecoded storage value of `:code` corresponding to the [`Runtime::runtime`] /// field. /// diff --git a/light-base/src/sync_service.rs b/light-base/src/sync_service.rs index 79cdece63d..8e18693e1e 100644 --- a/light-base/src/sync_service.rs +++ b/light-base/src/sync_service.rs @@ -70,12 +70,39 @@ pub struct Config { /// [`network_service::NetworkService::new`]. pub network_events_receiver: stream::BoxStream<'static, network_service::Event>, - /// Extra fields used when the chain is a parachain. - /// If `None`, this chain is a standalone chain or a relay chain. - pub parachain: Option>, + /// Extra fields depending on whether the chain is a relay chain or a parachain. + pub chain_type: ConfigChainType, } -/// See [`Config::parachain`]. +/// See [`Config::chain_type`]. +pub enum ConfigChainType { + /// Chain is a relay chain. + RelayChain(ConfigRelayChain), + /// Chain is a parachain. + Parachain(ConfigParachain), +} + +/// See [`ConfigChainType::RelayChain`]. +pub struct ConfigRelayChain { + /// Known valid Merkle value and storage value combination for the `:code` key. + /// + /// If provided, the warp syncing algorithm will first fetch the Merkle value of `:code`, and + /// if it matches the Merkle value provided in the hint, use the storage value in the hint + /// instead of downloading it. If the hint doesn't match, an extra round-trip will be needed, + /// but if the hint matches it saves a big download. + pub runtime_code_hint: Option, +} + +/// See [`ConfigRelayChain::runtime_code_hint`]. +pub struct ConfigRelayChainRuntimeCodeHint { + /// Storage value of the `:code` trie node corresponding to + /// [`ConfigRelayChainRuntimeCodeHint::code_merkle_value`]. + pub storage_value: Vec, + /// Merkle value of the `:code` trie node in the storage main trie. + pub merkle_value: Vec, +} + +/// See [`ConfigChainType::Parachain`]. pub struct ConfigParachain { /// Runtime service that synchronizes the relay chain of this parachain. pub relay_chain_sync: Arc>, @@ -115,36 +142,40 @@ impl SyncService { let log_target = format!("sync-service-{}", config.log_name); - if let Some(config_parachain) = config.parachain { - config.platform.spawn_task( - log_target.clone().into(), - Box::pin(parachain::start_parachain( - log_target, - config.platform.clone(), - config.chain_information, - config.block_number_bytes, - config_parachain.relay_chain_sync.clone(), - config_parachain.relay_chain_block_number_bytes, - config_parachain.parachain_id, - from_foreground, - config.network_service.1, - config.network_events_receiver, - )), - ); - } else { - config.platform.spawn_task( - log_target.clone().into(), - Box::pin(standalone::start_standalone_chain( - log_target, - config.platform.clone(), - config.chain_information, - config.block_number_bytes, - from_foreground, - config.network_service.0.clone(), - config.network_service.1, - config.network_events_receiver, - )), - ); + match config.chain_type { + ConfigChainType::Parachain(config_parachain) => { + config.platform.spawn_task( + log_target.clone().into(), + Box::pin(parachain::start_parachain( + log_target, + config.platform.clone(), + config.chain_information, + config.block_number_bytes, + config_parachain.relay_chain_sync.clone(), + config_parachain.relay_chain_block_number_bytes, + config_parachain.parachain_id, + from_foreground, + config.network_service.1, + config.network_events_receiver, + )), + ); + } + ConfigChainType::RelayChain(config_relay_chain) => { + config.platform.spawn_task( + log_target.clone().into(), + Box::pin(standalone::start_standalone_chain( + log_target, + config.platform.clone(), + config.chain_information, + config.block_number_bytes, + config_relay_chain.runtime_code_hint, + from_foreground, + config.network_service.0.clone(), + config.network_service.1, + config.network_events_receiver, + )), + ); + } } SyncService { @@ -961,6 +992,9 @@ pub struct FinalizedBlockRuntime { /// Storage value at the `:heappages` key. pub storage_heap_pages: Option>, + + /// Merkle value of the `:code` key. + pub code_merkle_value: Option>, } /// Notification about a new block or a new finalized block. diff --git a/light-base/src/sync_service/standalone.rs b/light-base/src/sync_service/standalone.rs index 5ed87992e8..c63c119b1c 100644 --- a/light-base/src/sync_service/standalone.rs +++ b/light-base/src/sync_service/standalone.rs @@ -15,7 +15,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use super::{BlockNotification, FinalizedBlockRuntime, Notification, SubscribeAll, ToBackground}; +use super::{ + BlockNotification, ConfigRelayChainRuntimeCodeHint, FinalizedBlockRuntime, Notification, + SubscribeAll, ToBackground, +}; use crate::{network_service, platform::PlatformRef, util}; use alloc::{borrow::ToOwned as _, string::String, sync::Arc, vec::Vec}; @@ -41,6 +44,7 @@ pub(super) async fn start_standalone_chain( platform: TPlat, chain_information: chain::chain_information::ValidChainInformation, block_number_bytes: usize, + runtime_code_hint: Option, mut from_foreground: mpsc::Receiver, network_service: Arc>, network_chain_index: usize, @@ -82,6 +86,10 @@ pub(super) async fn start_standalone_chain( NonZeroU32::new(5000).unwrap() }, full_mode: false, + code_trie_node_hint: runtime_code_hint.map(|hint| all::ConfigCodeTrieNodeHint { + merkle_value: hint.merkle_value, + storage_value: hint.storage_value, + }), }), network_up_to_date_best: true, network_up_to_date_finalized: true, @@ -642,6 +650,7 @@ impl Task { finalized_block_runtime, finalized_storage_code, finalized_storage_heap_pages, + finalized_storage_code_merkle_value, } => { self.sync = sync; @@ -661,6 +670,7 @@ impl Task { virtual_machine: finalized_block_runtime, storage_code: finalized_storage_code, storage_heap_pages: finalized_storage_heap_pages, + code_merkle_value: finalized_storage_code_merkle_value, }); self.network_up_to_date_finalized = false; From 3a69b51389d0f4ae6264ba62eec696fbec7cd963 Mon Sep 17 00:00:00 2001 From: Pierre Krieger Date: Thu, 6 Jul 2023 13:32:18 +0200 Subject: [PATCH 2/5] PR link --- wasm-node/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wasm-node/CHANGELOG.md b/wasm-node/CHANGELOG.md index 8afd19a211..442310aeba 100644 --- a/wasm-node/CHANGELOG.md +++ b/wasm-node/CHANGELOG.md @@ -4,7 +4,7 @@ ### Changed -- The runtime code of the finalized block is now stored in the database. At initialization, smoldot now only downloads the hash of the runtime and compares it with the one in cache. If the hashes match (which is the case if no runtime update has happened on the chain since the database has been created), smoldot doesn't download the runtime code but uses the value in the cache. This saves a relatively heavy download (typically around 1 MiB to 1.5 MiB depending on the chain) and speeds up the loading time. +- The runtime code of the finalized block is now stored in the database. At initialization, smoldot now only downloads the hash of the runtime and compares it with the one in cache. If the hashes match (which is the case if no runtime update has happened on the chain since the database has been created), smoldot doesn't download the runtime code but uses the value in the cache. This saves a relatively heavy download (typically around 1 MiB to 1.5 MiB depending on the chain) and speeds up the loading time. ([#863](https://github.com/smol-dot/smoldot/pull/863)) - The `chainHead_unstable_storage` JSON-RPC function now supports a `type` equal to `closest-descendant-merkle-value` and no longer supports `closest-ancestor-merkle-value`, in accordance with the latest changes in the JSON-RPC API specification. ([#824](https://github.com/smol-dot/smoldot/pull/824)) - Blocks are now reported to `chain_subscribeAllHeads` and `chain_subscribeNewHeads` subscribers only after they have been put in the cache, preventing race conditions where JSON-RPC clients suffer from a cache miss if they ask information about these blocks too quickly. ([#854](https://github.com/smol-dot/smoldot/pull/854)) - Runtime updates are now always reported to `state_subscribeRuntimeVersion` subscribers immediately after the `chain_subscribeNewHeads` notification corresponding to the block containing the runtime update. They were previously reported in a pseudo-random order. ([#854](https://github.com/smol-dot/smoldot/pull/854)) @@ -12,7 +12,7 @@ ### Fixed -- Fix downloading the runtime code twice during the warp syncing process. +- Fix downloading the runtime code twice during the warp syncing process. ([#863](https://github.com/smol-dot/smoldot/pull/863)) ## 1.0.11 - 2023-06-25 From 03aa5abc1f87a8e986d314be80259462bc3be126 Mon Sep 17 00:00:00 2001 From: Pierre Krieger Date: Thu, 6 Jul 2023 14:03:29 +0200 Subject: [PATCH 3/5] Fix full node compilation --- full-node/src/consensus_service.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/full-node/src/consensus_service.rs b/full-node/src/consensus_service.rs index 0b926a06a4..b9adadd561 100644 --- a/full-node/src/consensus_service.rs +++ b/full-node/src/consensus_service.rs @@ -241,6 +241,7 @@ impl ConsensusService { NonZeroU32::new(2000).unwrap() }, full_mode: true, + code_trie_node_hint: None, }); let finalized_runtime = { From ac4dde3d150ec97939ed7025cb6991ee6b6ac165 Mon Sep 17 00:00:00 2001 From: Pierre Krieger Date: Thu, 6 Jul 2023 15:03:45 +0200 Subject: [PATCH 4/5] Oops, 0xf is 15 not 16 --- lib/src/trie/nibble.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/trie/nibble.rs b/lib/src/trie/nibble.rs index 34efc30152..a7cdfa4583 100644 --- a/lib/src/trie/nibble.rs +++ b/lib/src/trie/nibble.rs @@ -343,9 +343,9 @@ mod tests { assert_eq!(u8::from(Nibble::from_ascii_hex_digit(b'0').unwrap()), 0); assert_eq!(u8::from(Nibble::from_ascii_hex_digit(b'9').unwrap()), 9); assert_eq!(u8::from(Nibble::from_ascii_hex_digit(b'a').unwrap()), 10); - assert_eq!(u8::from(Nibble::from_ascii_hex_digit(b'f').unwrap()), 16); + assert_eq!(u8::from(Nibble::from_ascii_hex_digit(b'f').unwrap()), 15); assert_eq!(u8::from(Nibble::from_ascii_hex_digit(b'A').unwrap()), 10); - assert_eq!(u8::from(Nibble::from_ascii_hex_digit(b'F').unwrap()), 16); + assert_eq!(u8::from(Nibble::from_ascii_hex_digit(b'F').unwrap()), 15); assert!(Nibble::from_ascii_hex_digit(b'j').is_none()); assert!(Nibble::from_ascii_hex_digit(b' ').is_none()); assert!(Nibble::from_ascii_hex_digit(0).is_none()); From 950409aeda16b560959b1fa51d13cab59c046f65 Mon Sep 17 00:00:00 2001 From: Pierre Krieger Date: Thu, 6 Jul 2023 15:22:38 +0200 Subject: [PATCH 5/5] Docfix --- light-base/src/sync_service.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/light-base/src/sync_service.rs b/light-base/src/sync_service.rs index e222625841..a58d46a954 100644 --- a/light-base/src/sync_service.rs +++ b/light-base/src/sync_service.rs @@ -96,7 +96,7 @@ pub struct ConfigRelayChain { /// See [`ConfigRelayChain::runtime_code_hint`]. pub struct ConfigRelayChainRuntimeCodeHint { /// Storage value of the `:code` trie node corresponding to - /// [`ConfigRelayChainRuntimeCodeHint::code_merkle_value`]. + /// [`ConfigRelayChainRuntimeCodeHint::merkle_value`]. pub storage_value: Vec, /// Merkle value of the `:code` trie node in the storage main trie. pub merkle_value: Vec, @@ -868,8 +868,8 @@ pub enum StorageResultItem { /// Key that was requested. Equal to the value of [`StorageRequestItem::key`]. requested_key: Vec, /// Closest ancestor to the requested key that was found in the proof. If - /// [`StorageResultItem::DescendantValue::closest_descendant_merkle_value`] is `Some`, then - /// this is always the parent of the requested key. + /// [`StorageResultItem::ClosestDescendantMerkleValue::closest_descendant_merkle_value`] + /// is `Some`, then this is always the parent of the requested key. found_closest_ancestor_excluding: Option>, /// Merkle value of the closest descendant of /// [`StorageResultItem::DescendantValue::requested_key`]. The key that corresponds