Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 114 additions & 6 deletions light-base/src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ pub struct DatabaseContent {
/// Does **not** necessarily match the finalized block found in
/// [`DatabaseContent::chain_information`].
pub runtime_code_hint: Option<DatabaseContentRuntimeCodeHint>,

/// Raw bytes of the runtime code (`:code` storage value) saved from the last session.
///
/// When present, a parachain warm-start can compile this code locally and verify
/// it against the network via lightweight Aura call proofs, skipping the ~2 MiB
/// P2P runtime download.
pub runtime_code: Option<Vec<u8>>,
}

/// See [`DatabaseContent::runtime_code_hint`].
Expand Down Expand Up @@ -207,23 +214,33 @@ pub fn decode_database(encoded: &str, block_number_bytes: usize) -> Result<Datab
})
.collect::<Vec<_>>();

// Decode the runtime code storage value (base64) once. Both `runtime_code_hint`
// and `runtime_code` derive from it, avoiding a redundant 2 MiB clone.
let decoded_code = decoded
.code_storage_value
.as_ref()
.map(|sv| {
base64::Engine::decode(&base64::engine::general_purpose::STANDARD_NO_PAD, sv)
.map_err(|_| ())
})
.transpose()?;

let runtime_code_hint = match (
decoded.code_merkle_value,
decoded.code_storage_value,
&decoded_code,
decoded.code_closest_ancestor_excluding,
) {
(Some(mv), Some(sv), Some(an)) => Some(DatabaseContentRuntimeCodeHint {
code: base64::Engine::decode(&base64::engine::general_purpose::STANDARD_NO_PAD, sv)
.map_err(|_| ())?,
(Some(mv), Some(code), Some(an)) => Some(DatabaseContentRuntimeCodeHint {
code: code.clone(),
code_merkle_value: hex::decode(mv).map_err(|_| ())?,
closest_ancestor_excluding: an
.as_bytes()
.iter()
.map(|char| Nibble::from_ascii_hex_digit(*char).ok_or(()))
.collect::<Result<Vec<Nibble>, ()>>()?,
}),
// A combination of `Some` and `None` is technically invalid, but we simply ignore this
// situation.
// A combination of `Some` and `None` is technically invalid, but we simply
// ignore this situation.
_ => None,
};

Expand All @@ -232,6 +249,7 @@ pub fn decode_database(encoded: &str, block_number_bytes: usize) -> Result<Datab
chain_information,
known_nodes,
runtime_code_hint,
runtime_code: decoded_code,
})
}

Expand Down Expand Up @@ -262,3 +280,93 @@ struct SerdeDatabase {
)]
code_closest_ancestor_excluding: Option<String>,
}

#[cfg(test)]
mod tests {
use super::*;

fn make_db_json(fields: &[(&str, &str)]) -> String {
let genesis = format!(r#""genesisHash":"{}""#, "aa".repeat(32));
let nodes = r#""nodes":{}"#;
let extras: Vec<String> = fields
.iter()
.map(|(k, v)| format!(r#""{k}":{v}"#))
.collect();
let mut parts = vec![genesis, nodes.to_string()];
parts.extend(extras);
format!("{{{}}}", parts.join(","))
}

#[test]
fn decode_database_without_runtime_code() {
let json = make_db_json(&[]);
let db = decode_database(&json, 4).unwrap();
assert!(db.runtime_code.is_none());
assert!(db.runtime_code_hint.is_none());
}

#[test]
fn decode_database_with_runtime_code_only() {
let code_bytes = b"\x00asm\x01\x00\x00\x00";
let encoded = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD_NO_PAD,
code_bytes,
);
let json = make_db_json(&[("runtimeCode", &format!(r#""{encoded}""#))]);
let db = decode_database(&json, 4).unwrap();
assert_eq!(db.runtime_code.as_deref(), Some(code_bytes.as_slice()));
assert!(db.runtime_code_hint.is_none());
}

#[test]
fn decode_database_with_full_hint_populates_both() {
let code_bytes = b"\x00asm\x01\x00\x00\x00";
let encoded_code = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD_NO_PAD,
code_bytes,
);
let merkle_hex = "ab".repeat(32);
let ancestor_nibbles = "3a636f";
let json = make_db_json(&[
("runtimeCode", &format!(r#""{encoded_code}""#)),
("codeMerkleValue", &format!(r#""{merkle_hex}""#)),
("codeClosestAncestor", &format!(r#""{ancestor_nibbles}""#)),
]);
let db = decode_database(&json, 4).unwrap();
assert_eq!(db.runtime_code.as_deref(), Some(code_bytes.as_slice()));
assert!(db.runtime_code_hint.is_some());
let hint = db.runtime_code_hint.unwrap();
assert_eq!(hint.code, code_bytes);
assert_eq!(hint.code_merkle_value, hex::decode(&merkle_hex).unwrap());
}

#[test]
fn decode_database_invalid_base64_runtime_code_returns_error() {
let json = make_db_json(&[("runtimeCode", r#""not-valid-base64!!!""#)]);
assert!(decode_database(&json, 4).is_err());
}

#[test]
fn encode_shrink_drops_runtime_code_when_too_large() {
let large_code = "A".repeat(10_000);
let db = SerdeDatabase {
genesis_hash: "aa".repeat(32),
chain: None,
nodes: Default::default(),
code_storage_value: Some(large_code),
code_merkle_value: Some("bb".repeat(32)),
code_closest_ancestor_excluding: None,
};
let serialized = serde_json::to_string(&db).unwrap();
assert!(serialized.len() > 1000);

let mut shrunk = db;
shrunk.code_storage_value = None;
shrunk.code_merkle_value = None;
let shrunk_serialized = serde_json::to_string(&shrunk).unwrap();
assert!(shrunk_serialized.len() < 200);

let decoded = decode_database(&shrunk_serialized, 4).unwrap();
assert!(decoded.runtime_code.is_none());
}
}
75 changes: 61 additions & 14 deletions light-base/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,9 +454,15 @@ impl<TPlat: platform::PlatformRef, TChain> Client<TPlat, TChain> {
// Load the information about the chain. If a light sync state (also known as a checkpoint)
// is present in the chain spec, it is possible to start syncing at the finalized block
// it describes.
// At the same time, we deconstruct the database into `known_nodes`
// and `runtime_code_hint`.
let (chain_information, used_database_chain_information, known_nodes, runtime_code_hint) = {
// At the same time, we deconstruct the database into `known_nodes`,
// `runtime_code_hint`, and `saved_runtime_code`.
let (
chain_information,
used_database_chain_information,
known_nodes,
runtime_code_hint,
saved_runtime_code,
) = {
let checkpoint = chain_spec
.light_sync_state()
.map(|s| s.to_chain_information());
Expand All @@ -471,12 +477,19 @@ impl<TPlat: platform::PlatformRef, TChain> Client<TPlat, TChain> {
chain_information: Some(db_ci),
known_nodes,
runtime_code_hint,
runtime_code,
..
}),
) if db_ci.as_ref().finalized_block_header.number
>= checkpoint.as_ref().finalized_block_header.number =>
{
(Some(db_ci), true, known_nodes, runtime_code_hint)
(
Some(db_ci),
true,
known_nodes,
runtime_code_hint,
runtime_code,
)
}

// Otherwise, use the chain spec checkpoint.
Expand All @@ -486,10 +499,19 @@ impl<TPlat: platform::PlatformRef, TChain> Client<TPlat, TChain> {
Some(database::DatabaseContent {
known_nodes,
runtime_code_hint,
runtime_code,
..
}),
) => (Some(checkpoint), false, known_nodes, runtime_code_hint),
(_, Some(Ok(checkpoint)), None) => (Some(checkpoint), false, Vec::new(), None),
) => (
Some(checkpoint),
false,
known_nodes,
runtime_code_hint,
runtime_code,
),
(_, Some(Ok(checkpoint)), None) => {
(Some(checkpoint), false, Vec::new(), None, None)
}

// If neither the genesis chain information nor the checkpoint chain information
// is available, we could in principle use the database, but for API reasons we
Expand All @@ -501,10 +523,11 @@ impl<TPlat: platform::PlatformRef, TChain> Client<TPlat, TChain> {
Some(database::DatabaseContent {
known_nodes,
runtime_code_hint,
runtime_code,
..
}),
) => (None, false, known_nodes, runtime_code_hint),
(None, None, None) => (None, false, Vec::new(), None),
) => (None, false, known_nodes, runtime_code_hint, runtime_code),
(None, None, None) => (None, false, Vec::new(), None, None),

// Use the genesis block if no checkpoint is available.
(
Expand All @@ -516,17 +539,24 @@ impl<TPlat: platform::PlatformRef, TChain> Client<TPlat, TChain> {
Some(database::DatabaseContent {
known_nodes,
runtime_code_hint,
runtime_code,
..
}),
) => (Some(genesis_ci), false, known_nodes, runtime_code_hint),
) => (
Some(genesis_ci),
false,
known_nodes,
runtime_code_hint,
runtime_code,
),
(
Some(genesis_ci),
None
| Some(Err(
chain_spec::CheckpointToChainInformationError::GenesisBlockCheckpoint,
)),
None,
) => (Some(genesis_ci), false, Vec::new(), None),
) => (Some(genesis_ci), false, Vec::new(), None, None),

// If the checkpoint format is invalid, we return an error no matter whether the
// genesis chain information could be used.
Expand Down Expand Up @@ -706,10 +736,24 @@ impl<TPlat: platform::PlatformRef, TChain> Client<TPlat, TChain> {
);

let config = match (&relay_chain, &chain_information) {
(Some((relay_chain, para_id, _)), _) => StartServicesChainTy::Parachain {
relay_chain,
para_id: *para_id,
},
(Some((relay_chain, para_id, _)), _) => {
if let Some(code) = &saved_runtime_code {
log!(
&self.platform,
Debug,
"smoldot",
format!(
"Parachain warm-start available: cached runtime={}KB",
code.len() / 1024,
)
);
}
StartServicesChainTy::Parachain {
relay_chain,
para_id: *para_id,
saved_runtime_code: saved_runtime_code.clone(),
}
}
(None, Some(chain_information)) => {
StartServicesChainTy::SubstrateCompatible { chain_information }
}
Expand Down Expand Up @@ -1117,6 +1161,7 @@ enum StartServicesChainTy<'a, TPlat: platform::PlatformRef> {
Parachain {
relay_chain: &'a ChainServices<TPlat>,
para_id: u32,
saved_runtime_code: Option<Vec<u8>>,
},
}

Expand Down Expand Up @@ -1185,6 +1230,7 @@ fn start_services<TPlat: platform::PlatformRef>(
StartServicesChainTy::Parachain {
relay_chain,
para_id,
saved_runtime_code,
} => {
// Chain is a parachain.

Expand All @@ -1202,6 +1248,7 @@ fn start_services<TPlat: platform::PlatformRef>(
para_id,
relay_chain_sync: relay_chain.runtime_service.clone(),
},
saved_runtime_code,
},
),
}));
Expand Down
8 changes: 8 additions & 0 deletions light-base/src/sync_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ pub struct ConfigSubstrateCompatibleRuntimeCodeHint {
pub struct ConfigParachain<TPlat: PlatformRef> {
/// Parameters of the relay chain.
pub relay_chain: ConfigRelayChain<TPlat>,

/// Raw bytes of the runtime (`:code` storage value) saved in the database.
///
/// When `Some`, `start_parachain` compiles this code locally and verifies it
/// against the network via lightweight Aura call proofs, skipping the ~2 MiB
/// P2P download. Falls back to cold bootstrap on any failure.
pub saved_runtime_code: Option<Vec<u8>>,
}

/// See [`ConfigParachain::relay_chain`].
Expand Down Expand Up @@ -154,6 +161,7 @@ impl<TPlat: PlatformRef> SyncService<TPlat> {
config_parachain.relay_chain.para_id,
from_foreground,
config.network_service.clone(),
config_parachain.saved_runtime_code,
)),
ConfigChainType::SubstrateCompatible(config_substrate_compat) => {
Box::pin(substrate_compat::start_substrate_compatible_chain(
Expand Down
Loading
Loading