use crate::{
LocalStorageLayer,
error::ExecutorError,
local::LocalSharedValue,
strings::executor::{magic_signature, storage_prefixes},
};
use smoldot::{
executor::{
self,
host::{Config as HostConfig, HostVmPrototype},
runtime_call::{self, OffchainContext, RuntimeCall},
storage_diff::TrieDiff,
vm::{ExecHint, HeapPages},
},
trie::{TrieEntryVersion, bytes_to_nibbles, nibbles_to_bytes_suffix_extend},
};
use std::{collections::BTreeMap, iter, iter::Once, sync::Arc};
struct ArcLocalSharedValue(Arc<LocalSharedValue>);
impl AsRef<[u8]> for ArcLocalSharedValue {
fn as_ref(&self) -> &[u8] {
self.0.as_ref().as_ref()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum SignatureMockMode {
#[default]
None,
MagicSignature,
AlwaysValid,
}
#[derive(Debug, Clone)]
pub struct RuntimeCallResult {
pub output: Vec<u8>,
pub storage_diff: Vec<(Vec<u8>, Option<Vec<u8>>)>,
pub offchain_storage_diff: Vec<(Vec<u8>, Option<Vec<u8>>)>,
pub logs: Vec<RuntimeLog>,
}
#[derive(Debug, Clone)]
pub struct RuntimeLog {
pub message: String,
pub level: Option<u32>,
pub target: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ExecutorConfig {
pub signature_mock: SignatureMockMode,
pub allow_unresolved_imports: bool,
pub max_log_level: u32,
pub storage_proof_size: u64,
}
impl Default for ExecutorConfig {
fn default() -> Self {
Self {
signature_mock: SignatureMockMode::MagicSignature,
allow_unresolved_imports: false,
max_log_level: 3, storage_proof_size: 0,
}
}
}
#[derive(Clone)]
pub struct RuntimeExecutor {
runtime_code: Arc<[u8]>,
heap_pages: HeapPages,
config: ExecutorConfig,
}
impl RuntimeExecutor {
pub fn new(
runtime_code: impl Into<Arc<[u8]>>,
heap_pages: Option<u32>,
) -> Result<Self, ExecutorError> {
let runtime_code: Arc<[u8]> = runtime_code.into();
let heap_pages = heap_pages.map(HeapPages::from).unwrap_or(executor::DEFAULT_HEAP_PAGES);
let _prototype = HostVmPrototype::new(HostConfig {
module: &runtime_code,
heap_pages,
exec_hint: ExecHint::ValidateAndExecuteOnce,
allow_unresolved_imports: false,
})?;
Ok(Self { runtime_code, heap_pages, config: ExecutorConfig::default() })
}
pub fn with_config(
runtime_code: impl Into<Arc<[u8]>>,
heap_pages: Option<u32>,
config: ExecutorConfig,
) -> Result<Self, ExecutorError> {
let mut executor = Self::new(runtime_code, heap_pages)?;
executor.config = config;
Ok(executor)
}
pub fn create_prototype(&self) -> Result<HostVmPrototype, ExecutorError> {
Ok(HostVmPrototype::new(HostConfig {
module: &self.runtime_code,
heap_pages: self.heap_pages,
exec_hint: ExecHint::ValidateAndCompile,
allow_unresolved_imports: self.config.allow_unresolved_imports,
})?)
}
pub async fn call_with_prototype(
&self,
prototype: Option<HostVmPrototype>,
method: &str,
args: &[u8],
storage: &LocalStorageLayer,
) -> (Result<RuntimeCallResult, ExecutorError>, Option<HostVmPrototype>) {
let vm_proto = match prototype {
Some(proto) => proto,
None => match self.create_prototype() {
Ok(proto) => proto,
Err(e) => return (Err(e), None),
},
};
let mut vm = match runtime_call::run(runtime_call::Config {
virtual_machine: vm_proto,
function_to_call: method,
parameter: iter::once(args),
storage_main_trie_changes: TrieDiff::default(),
max_log_level: self.config.max_log_level,
calculate_trie_changes: false,
storage_proof_size_behavior:
runtime_call::StorageProofSizeBehavior::ConstantReturnValue(
self.config.storage_proof_size,
),
}) {
Ok(vm) => vm,
Err((err, proto)) => {
return (
Err(ExecutorError::StartError {
method: method.to_string(),
message: err.to_string(),
}),
Some(proto),
);
},
};
let mut storage_changes: BTreeMap<Vec<u8>, Option<Vec<u8>>> = BTreeMap::new();
let mut offchain_storage_changes: BTreeMap<Vec<u8>, Option<Vec<u8>>> = BTreeMap::new();
let mut logs: Vec<RuntimeLog> = Vec::new();
loop {
vm = match vm {
RuntimeCall::Finished(result) => {
return match result {
Ok(success) => {
success.storage_changes.storage_changes_iter_unordered().for_each(
|(child, key, value)| {
let prefixed_key = if let Some(child) = child {
prefixed_child_key(
child.iter().copied(),
key.iter().copied(),
)
} else {
key.to_vec()
};
storage_changes.insert(prefixed_key, value.map(|v| v.to_vec()));
},
);
let output = success.virtual_machine.value().as_ref().to_vec();
let proto = success.virtual_machine.into_prototype();
(
Ok(RuntimeCallResult {
output,
storage_diff: storage_changes.into_iter().collect(),
offchain_storage_diff: offchain_storage_changes
.into_iter()
.collect(),
logs,
}),
Some(proto),
)
},
Err(err) => {
let proto = err.prototype;
(Err(err.detail.into()), Some(proto))
},
};
},
RuntimeCall::StorageGet(req) => {
let key = if let Some(child) = req.child_trie() {
prefixed_child_key(
child.as_ref().iter().copied(),
req.key().as_ref().iter().copied(),
)
} else {
req.key().as_ref().to_vec()
};
if let Some(value) = storage_changes.get(&key) {
req.inject_value(
value.as_ref().map(|v| (iter::once(v), TrieEntryVersion::V1)),
)
} else {
let block_number = storage.get_current_block_number();
let value = match storage.get(block_number, &key).await {
Ok(v) => v,
Err(e) => {
return (
Err(ExecutorError::StorageError {
key: hex::encode(&key),
message: e.to_string(),
}),
None,
);
},
};
let none_placeholder: Option<(Once<[u8; 0]>, TrieEntryVersion)> = None;
match value {
Some(value) if value.value.is_some() => req.inject_value(Some((
iter::once(ArcLocalSharedValue(value)),
TrieEntryVersion::V1,
))),
_ => req.inject_value(none_placeholder),
}
}
},
RuntimeCall::ClosestDescendantMerkleValue(req) => {
static FAKE_MERKLE: [u8; 32] = [0u8; 32];
req.inject_merkle_value(Some(&FAKE_MERKLE))
},
RuntimeCall::NextKey(req) =>
if req.branch_nodes() {
req.inject_key(None::<Vec<_>>.map(|x| x.into_iter()))
} else {
let prefix = if let Some(child) = req.child_trie() {
prefixed_child_key(
child.as_ref().iter().copied(),
nibbles_to_bytes_suffix_extend(req.prefix()),
)
} else {
nibbles_to_bytes_suffix_extend(req.prefix()).collect::<Vec<_>>()
};
let key = if let Some(child) = req.child_trie() {
prefixed_child_key(
child.as_ref().iter().copied(),
nibbles_to_bytes_suffix_extend(req.key()),
)
} else {
nibbles_to_bytes_suffix_extend(req.key()).collect::<Vec<_>>()
};
let next = match storage.next_key(&prefix, &key).await {
Ok(v) => v,
Err(e) => {
return (
Err(ExecutorError::StorageError {
key: hex::encode(&key),
message: e.to_string(),
}),
None,
);
},
};
req.inject_key(next.map(|k| bytes_to_nibbles(k.into_iter())))
},
RuntimeCall::SignatureVerification(req) => match self.config.signature_mock {
SignatureMockMode::MagicSignature => {
if is_magic_signature(req.signature().as_ref()) {
req.resume_success()
} else {
req.verify_and_resume()
}
},
SignatureMockMode::AlwaysValid => req.resume_success(),
SignatureMockMode::None => req.verify_and_resume(),
},
RuntimeCall::OffchainStorageSet(req) => {
offchain_storage_changes.insert(
req.key().as_ref().to_vec(),
req.value().map(|x| x.as_ref().to_vec()),
);
req.resume()
},
RuntimeCall::Offchain(ctx) => match ctx {
OffchainContext::StorageGet(req) => {
let key = req.key().as_ref().to_vec();
let value = offchain_storage_changes.get(&key).cloned().flatten();
req.inject_value(value)
},
OffchainContext::StorageSet(req) => {
let key = req.key().as_ref().to_vec();
let current = offchain_storage_changes.get(&key);
let replace = match (current, req.old_value()) {
(Some(Some(current)), Some(old)) => old.as_ref().eq(current),
_ => true,
};
if replace {
offchain_storage_changes
.insert(key, req.value().map(|x| x.as_ref().to_vec()));
}
req.resume(replace)
},
OffchainContext::Timestamp(req) => {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
req.inject_timestamp(timestamp)
},
OffchainContext::RandomSeed(req) => {
let seed = sp_core::blake2_256(
&std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos().to_le_bytes())
.unwrap_or([0u8; 16]),
);
req.inject_random_seed(seed)
},
OffchainContext::SubmitTransaction(req) => req.resume(false),
},
RuntimeCall::LogEmit(req) => {
use smoldot::executor::host::LogEmitInfo;
let log = match req.info() {
LogEmitInfo::Num(v) =>
RuntimeLog { message: format!("{}", v), level: None, target: None },
LogEmitInfo::Utf8(v) =>
RuntimeLog { message: v.to_string(), level: None, target: None },
LogEmitInfo::Hex(v) =>
RuntimeLog { message: v.to_string(), level: None, target: None },
LogEmitInfo::Log { log_level, target, message } => RuntimeLog {
message: message.to_string(),
level: Some(log_level),
target: Some(target.to_string()),
},
};
logs.push(log);
req.resume()
},
}
}
}
pub async fn call(
&self,
method: &str,
args: &[u8],
storage: &LocalStorageLayer,
) -> Result<RuntimeCallResult, ExecutorError> {
self.call_with_prototype(None, method, args, storage).await.0
}
pub fn runtime_version(&self) -> Result<RuntimeVersion, ExecutorError> {
let prototype = HostVmPrototype::new(HostConfig {
module: &self.runtime_code,
heap_pages: self.heap_pages,
exec_hint: ExecHint::ValidateAndExecuteOnce,
allow_unresolved_imports: true,
})?;
let version = prototype.runtime_version().decode();
Ok(RuntimeVersion {
spec_name: version.spec_name.to_string(),
impl_name: version.impl_name.to_string(),
authoring_version: version.authoring_version,
spec_version: version.spec_version,
impl_version: version.impl_version,
transaction_version: version.transaction_version.unwrap_or(0),
state_version: version.state_version.map(|v| v.into()).unwrap_or(0),
})
}
}
#[derive(Debug, Clone)]
pub struct RuntimeVersion {
pub spec_name: String,
pub impl_name: String,
pub authoring_version: u32,
pub spec_version: u32,
pub impl_version: u32,
pub transaction_version: u32,
pub state_version: u8,
}
fn prefixed_child_key(child: impl Iterator<Item = u8>, key: impl Iterator<Item = u8>) -> Vec<u8> {
[storage_prefixes::DEFAULT_CHILD_STORAGE, &child.collect::<Vec<_>>(), &key.collect::<Vec<_>>()]
.concat()
}
fn is_magic_signature(signature: &[u8]) -> bool {
signature.starts_with(magic_signature::PREFIX) &&
signature[magic_signature::PREFIX.len()..]
.iter()
.all(|&b| b == magic_signature::PADDING)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn magic_signature_accepts_valid_patterns() {
assert!(is_magic_signature(&[0xde, 0xad, 0xbe, 0xef, 0xcd, 0xcd]));
assert!(is_magic_signature(&[0xde, 0xad, 0xbe, 0xef, 0xcd, 0xcd, 0xcd, 0xcd]));
assert!(is_magic_signature(&[0xde, 0xad, 0xbe, 0xef]));
assert!(!is_magic_signature(&[0xde, 0xad, 0xbe, 0xef, 0xcd, 0xcd, 0xcd, 0x00]));
assert!(!is_magic_signature(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]));
assert!(!is_magic_signature(&[0xde, 0xad, 0xbe])); }
#[test]
fn prefixed_child_key_combines_prefix_child_and_key() {
let child = b"child1".iter().copied();
let key = b"key1".iter().copied();
let result = prefixed_child_key(child, key);
assert!(result.starts_with(storage_prefixes::DEFAULT_CHILD_STORAGE));
assert!(result.ends_with(b"key1"));
}
#[test]
fn executor_config_has_sensible_defaults() {
let config = ExecutorConfig::default();
assert_eq!(config.signature_mock, SignatureMockMode::MagicSignature);
assert!(!config.allow_unresolved_imports);
assert_eq!(config.max_log_level, 3);
assert_eq!(config.storage_proof_size, 0);
}
#[test]
fn signature_mock_mode_defaults_to_none() {
let mode = SignatureMockMode::default();
assert_eq!(mode, SignatureMockMode::None);
}
mod sequential {
use crate::{LocalStorageLayer, testing::TestContext};
use scale::Encode;
use subxt::config::substrate::H256;
use super::*;
struct ExecutorTestContext {
#[allow(dead_code)]
base: TestContext,
executor: RuntimeExecutor,
storage: LocalStorageLayer,
block_hash: H256,
block_number: u32,
}
async fn create_executor_context() -> ExecutorTestContext {
create_executor_context_with_config(ExecutorConfig::default()).await
}
async fn create_executor_context_with_config(
config: ExecutorConfig,
) -> ExecutorTestContext {
let base = TestContext::for_local().await;
let block_hash = base.block_hash();
let block_number = base.block_number();
let header = base.rpc().header(block_hash).await.expect("Failed to get header");
let runtime_code =
base.rpc().runtime_code(block_hash).await.expect("Failed to fetch runtime code");
base.cache()
.cache_block(block_hash, block_number, header.parent_hash, &header.encode())
.await
.expect("Failed to cache block");
let storage = LocalStorageLayer::new(
base.remote().clone(),
block_number,
block_hash,
base.metadata().clone(),
);
let executor = RuntimeExecutor::with_config(runtime_code, None, config)
.expect("Failed to create executor");
ExecutorTestContext { base, executor, storage, block_hash, block_number }
}
#[tokio::test(flavor = "multi_thread")]
async fn core_version_executes_successfully() {
let ctx = create_executor_context().await;
let result = ctx
.executor
.call("Core_version", &[], &ctx.storage)
.await
.expect("Core_version execution failed");
assert!(!result.output.is_empty(), "Core_version should return non-empty output");
assert!(result.storage_diff.is_empty(), "Core_version should not modify storage");
let version = ctx.executor.runtime_version().expect("Failed to get runtime version");
assert!(!version.spec_name.is_empty(), "spec_name should not be empty");
assert!(version.spec_version > 0, "spec_version should be positive");
}
#[tokio::test(flavor = "multi_thread")]
async fn metadata_executes_successfully() {
let ctx = create_executor_context().await;
let result = ctx
.executor
.call("Metadata_metadata", &[], &ctx.storage)
.await
.expect("Metadata_metadata execution failed");
assert!(
result.output.len() > 1000,
"Metadata should be larger than 1KB, got {} bytes",
result.output.len()
);
assert!(result.storage_diff.is_empty(), "Metadata_metadata should not modify storage");
}
#[tokio::test(flavor = "multi_thread")]
async fn with_config_applies_custom_settings() {
let config = ExecutorConfig {
signature_mock: SignatureMockMode::AlwaysValid,
allow_unresolved_imports: false,
max_log_level: 5, storage_proof_size: 1024,
};
let ctx = create_executor_context_with_config(config).await;
let result = ctx
.executor
.call("Core_version", &[], &ctx.storage)
.await
.expect("Core_version with custom config failed");
assert!(!result.output.is_empty(), "Should return output with custom config");
}
#[tokio::test(flavor = "multi_thread")]
async fn logs_are_captured_during_execution() {
let config = ExecutorConfig {
max_log_level: 5, ..Default::default()
};
let ctx = create_executor_context_with_config(config).await;
let result = ctx
.executor
.call("Metadata_metadata", &[], &ctx.storage)
.await
.expect("Metadata_metadata execution failed");
println!("Captured {} runtime logs", result.logs.len());
for log in &result.logs {
println!(
" [{:?}] {}: {}",
log.level,
log.target.as_deref().unwrap_or("unknown"),
log.message
);
}
assert!(result.output.len() > 1000, "Metadata should still be returned");
}
#[tokio::test(flavor = "multi_thread")]
async fn core_initialize_block_modifies_storage() {
let ctx = create_executor_context().await;
let next_block_number = ctx.block_number + 1;
#[derive(Encode)]
struct Header {
parent_hash: H256,
#[codec(compact)]
number: u32,
state_root: H256,
extrinsics_root: H256,
digest: Vec<DigestItem>,
}
#[derive(Encode)]
enum DigestItem {
#[codec(index = 6)]
PreRuntime([u8; 4], Vec<u8>),
}
let header = Header {
parent_hash: ctx.block_hash,
number: next_block_number,
state_root: H256::zero(), extrinsics_root: H256::zero(), digest: vec![
DigestItem::PreRuntime(*b"aura", 0u64.encode()),
],
};
let result = ctx
.executor
.call("Core_initialize_block", &header.encode(), &ctx.storage)
.await
.expect("Core_initialize_block execution failed");
assert!(
!result.storage_diff.is_empty(),
"Core_initialize_block should modify storage, got {} changes",
result.storage_diff.len()
);
let system_prefix = sp_core::twox_128(b"System");
let number_key = sp_core::twox_128(b"Number");
let system_number_key: Vec<u8> =
[system_prefix.as_slice(), number_key.as_slice()].concat();
let has_number_update =
result.storage_diff.iter().any(|(key, _)| key == &system_number_key);
assert!(
has_number_update,
"Core_initialize_block should update System::Number. Keys modified: {:?}",
result.storage_diff.iter().map(|(k, _)| hex::encode(k)).collect::<Vec<_>>()
);
println!("Core_initialize_block modified {} storage keys", result.storage_diff.len());
}
#[tokio::test(flavor = "multi_thread")]
async fn storage_reads_from_accumulated_changes() {
let ctx = create_executor_context().await;
#[derive(Encode)]
struct Header {
parent_hash: H256,
#[codec(compact)]
number: u32,
state_root: H256,
extrinsics_root: H256,
digest: Vec<DigestItem>,
}
#[derive(Encode)]
enum DigestItem {
#[codec(index = 6)]
PreRuntime([u8; 4], Vec<u8>),
}
let header = Header {
parent_hash: ctx.block_hash,
number: ctx.block_number + 1,
state_root: H256::zero(),
extrinsics_root: H256::zero(),
digest: vec![DigestItem::PreRuntime(*b"aura", 0u64.encode())],
};
let result = ctx
.executor
.call("Core_initialize_block", &header.encode(), &ctx.storage)
.await
.expect("Core_initialize_block execution failed");
assert!(!result.storage_diff.is_empty(), "Should have storage changes");
}
#[tokio::test(flavor = "multi_thread")]
async fn storage_changes_persist_across_calls() {
let ctx = create_executor_context().await;
#[derive(Encode)]
struct Header {
parent_hash: H256,
#[codec(compact)]
number: u32,
state_root: H256,
extrinsics_root: H256,
digest: Vec<DigestItem>,
}
#[derive(Encode)]
enum DigestItem {
#[codec(index = 6)]
PreRuntime([u8; 4], Vec<u8>),
}
let header = Header {
parent_hash: ctx.block_hash,
number: ctx.block_number + 1,
state_root: H256::zero(),
extrinsics_root: H256::zero(),
digest: vec![DigestItem::PreRuntime(*b"aura", 0u64.encode())],
};
let init_result = ctx
.executor
.call("Core_initialize_block", &header.encode(), &ctx.storage)
.await
.expect("Core_initialize_block failed");
assert!(!init_result.storage_diff.is_empty(), "Init should write storage");
for (key, value) in &init_result.storage_diff {
ctx.storage.set(key, value.as_deref()).expect("Failed to apply storage change");
}
let system_prefix = sp_core::twox_128(b"System");
let number_key = sp_core::twox_128(b"Number");
let system_number_key: Vec<u8> =
[system_prefix.as_slice(), number_key.as_slice()].concat();
let block_num = ctx.storage.get_current_block_number();
let stored_value = ctx
.storage
.get(block_num, &system_number_key)
.await
.expect("Failed to read System::Number");
assert!(
stored_value.is_some(),
"System::Number should be set after Core_initialize_block"
);
println!(
"Storage changes persist: {} keys modified, System::Number set",
init_result.storage_diff.len()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn runtime_version_extracts_version_info() {
let ctx = create_executor_context().await;
let version = ctx.executor.runtime_version().expect("runtime_version should succeed");
assert!(!version.spec_name.is_empty(), "spec_name should not be empty");
assert!(!version.impl_name.is_empty(), "impl_name should not be empty");
assert!(version.spec_version > 0, "spec_version should be positive");
println!(
"Runtime version: {} v{} (impl: {} v{})",
version.spec_name, version.spec_version, version.impl_name, version.impl_version
);
}
}
}