diff --git a/Cargo.lock b/Cargo.lock index d38ab70..73208d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -505,6 +505,17 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +[[package]] +name = "lookup" +version = "0.0.15-pre.1" +dependencies = [ + "cjam", + "jade", + "nauth", + "serde", + "serde-jam", +] + [[package]] name = "matchers" version = "0.2.0" diff --git a/crates/jade/src/host/general.rs b/crates/jade/src/host/general.rs index 143beac..2cbe97a 100644 --- a/crates/jade/src/host/general.rs +++ b/crates/jade/src/host/general.rs @@ -27,6 +27,7 @@ pub mod fetch { /// Storage operations pub mod storage { use super::*; + use crate::prelude::Vec; use anyhow::Result; /// Read a value from the storage @@ -75,4 +76,29 @@ pub mod storage { Ok(()) } + + /// Lookup a preimage by its hash within the current service. + pub fn lookup(hash: impl AsRef<[u8]>) -> Option> { + lookup_at(u64::MAX, hash) + } + + /// Lookup a preimage by its hash stored under a specific service. + pub fn lookup_at(service: u64, hash: impl AsRef<[u8]>) -> Option> { + let hash = hash.as_ref(); + debug_assert!(!hash.is_empty(), "preimage hash must not be empty"); + let len = unsafe { import::lookup(service, hash.as_ptr(), core::ptr::null_mut(), 0, 0) }; + + if len == u64::MAX || len == 0 { + return None; + } + + if len > usize::MAX as u64 { + return None; + } + + let ptr = unsafe { import::lookup(service, hash.as_ptr(), core::ptr::null_mut(), 0, len) }; + + let data = unsafe { core::slice::from_raw_parts(ptr as *const u8, len as usize) }; + Some(data.to_vec()) + } } diff --git a/crates/jade/src/host/import.rs b/crates/jade/src/host/import.rs index d1b3624..531172c 100644 --- a/crates/jade/src/host/import.rs +++ b/crates/jade/src/host/import.rs @@ -20,6 +20,16 @@ extern "C" { #[polkavm_import(index = 1)] pub fn fetch(buffer: *mut u8, offset: u64, buffer_len: u64, kind: u64, a: u64, b: u64) -> u64; + /// Retrieve a preimage by hash. + #[polkavm_import(index = 2)] + pub fn lookup( + service: u64, + hash_ptr: *const u8, + out: *mut u8, + offset: u64, + out_len: u64, + ) -> u64; + /// Read a value from the storage #[polkavm_import(index = 3)] pub fn read( diff --git a/services/lookup/Cargo.toml b/services/lookup/Cargo.toml new file mode 100644 index 0000000..0dcfeeb --- /dev/null +++ b/services/lookup/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "lookup" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +codec.workspace = true +jade = { workspace = true, features = ["logging"] } +serde.workspace = true + +[dev-dependencies] +nauth.workspace = true + +[build-dependencies] +cjam.workspace = true + +[features] +default = [] +tiny = ["jade/tiny"] +std = ["codec/std"] diff --git a/services/lookup/build.rs b/services/lookup/build.rs new file mode 100644 index 0000000..96d47c5 --- /dev/null +++ b/services/lookup/build.rs @@ -0,0 +1,5 @@ +//! Build the service + +fn main() { + cjam::build(env!("CARGO_PKG_NAME"), Some(cjam::ModuleType::Service)).ok(); +} diff --git a/services/lookup/src/instruction.rs b/services/lookup/src/instruction.rs new file mode 100644 index 0000000..e9a43d7 --- /dev/null +++ b/services/lookup/src/instruction.rs @@ -0,0 +1,13 @@ +//! Instructions for the lookup service. + +use jade::prelude::OpaqueHash; +use serde::{Deserialize, Serialize}; + +/// Commands that the lookup service can execute. +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy)] +pub enum Instruction { + /// Lookup a preimage stored under the current service account. + Lookup { hash: OpaqueHash }, + /// Lookup a preimage stored under the specified service account. + LookupFrom { service: u64, hash: OpaqueHash }, +} diff --git a/services/lookup/src/lib.rs b/services/lookup/src/lib.rs new file mode 100644 index 0000000..16fe9e4 --- /dev/null +++ b/services/lookup/src/lib.rs @@ -0,0 +1,12 @@ +#![cfg_attr(any(target_arch = "riscv32", target_arch = "riscv64"), no_std)] + +pub use instruction::Instruction; +pub use storage::{LookupStore, LookupTarget}; + +pub mod instruction; +mod service; +pub mod storage; + +/// The service blob for the lookup service. +#[cfg(not(any(target_arch = "riscv32", target_arch = "riscv64")))] +pub const SERVICE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/service.jam")); diff --git a/services/lookup/src/service.rs b/services/lookup/src/service.rs new file mode 100644 index 0000000..4ea1897 --- /dev/null +++ b/services/lookup/src/service.rs @@ -0,0 +1,121 @@ +//! Lookup service implementation. + +use crate::{ + Instruction, + storage::{LookupStore, LookupTarget}, +}; +use jade::{ + error, + host::storage, + info, + prelude::Vec, + service::{ + OpaqueHash, + service::WorkExecResult, + vm::{AccumulateItem, Operand}, + }, +}; + +#[jade::refine] +fn refine( + _core: u16, + _index: u16, + _id: u32, + payload: Vec, + _package_hash: OpaqueHash, +) -> Vec { + if let Ok(instructions) = codec::decode::>(payload.as_slice()) { + info!( + target = "lookup-service", + "refine payload decoded into {} instructions", + instructions.len() + ); + payload + } else { + error!(target = "lookup-service", "failed to decode instructions"); + Vec::new() + } +} + +#[jade::accumulate] +fn accumulate(_now: u32, id: u32, items: Vec) -> Option { + let mut store = LookupStore::get(); + let mut updated = false; + let service_id = u64::from(id); + + for raw in items.into_iter().filter_map(|item| { + if let AccumulateItem::Operand(Operand { + data: WorkExecResult::Ok(data), + .. + }) = item + { + Some(data) + } else { + None + } + }) { + let Ok(instructions) = codec::decode::>(&raw) else { + error!( + target = "lookup-service", + "failed to decode instructions during accumulate" + ); + continue; + }; + + for instruction in instructions { + match instruction { + Instruction::Lookup { hash } => { + let target = LookupTarget { + service: service_id, + hash, + }; + if store.contains(&target) { + continue; + } + + match storage::lookup(hash) { + Some(preimage) => { + info!( + target = "lookup-service", + "cached preimage from current service" + ); + store.put(target, preimage); + updated = true; + } + None => error!( + target = "lookup-service", + "preimage not found in current service storage" + ), + } + } + Instruction::LookupFrom { service, hash } => { + let target = LookupTarget { service, hash }; + if store.contains(&target) { + continue; + } + + match storage::lookup_at(service, hash) { + Some(preimage) => { + info!( + target = "lookup-service", + "cached preimage from service {}", service + ); + store.put(target, preimage); + updated = true; + } + None => error!( + target = "lookup-service", + "preimage not found in service {}", service + ), + } + } + } + } + } + + if updated { + store.save(); + } + + None +} diff --git a/services/lookup/src/storage.rs b/services/lookup/src/storage.rs new file mode 100644 index 0000000..4efa227 --- /dev/null +++ b/services/lookup/src/storage.rs @@ -0,0 +1,65 @@ +//! Storage helpers for the lookup service. + +use jade::{ + error, + host::storage, + prelude::{BTreeMap, OpaqueHash, Vec}, +}; +use serde::{Deserialize, Serialize}; + +/// Identifier for a stored preimage. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct LookupTarget { + /// Service identifier where the preimage resides. + pub service: u64, + /// Hash of the preimage. + pub hash: OpaqueHash, +} + +/// Collection of cached preimages. +#[derive(Serialize, Deserialize, Default)] +pub struct LookupStore { + entries: BTreeMap>, +} + +impl LookupStore { + /// Load the stored entries from persistent storage. + pub fn get() -> Self { + storage::read(Self::key()).unwrap_or_default() + } + + /// Persist the store. + pub fn save(&self) { + if let Err(err) = storage::write(Self::key(), self) { + error!( + target = "lookup-service", + "failed to save lookup store: {:?}", err + ); + } + } + + /// Insert or replace a cached preimage. + pub fn put(&mut self, target: LookupTarget, preimage: Vec) { + self.entries.insert(target, preimage); + } + + /// Retrieve a cached preimage. + pub fn get_entry(&self, target: &LookupTarget) -> Option<&[u8]> { + self.entries.get(target).map(Vec::as_slice) + } + + /// Check if an entry exists. + pub fn contains(&self, target: &LookupTarget) -> bool { + self.entries.contains_key(target) + } + + /// Enumerate all cached entries. + pub fn entries(&self) -> &BTreeMap> { + &self.entries + } + + /// Storage key for the lookup store. + pub const fn key() -> &'static [u8] { + b"lookup::store" + } +} diff --git a/services/lookup/tests/main.rs b/services/lookup/tests/main.rs new file mode 100644 index 0000000..2b7208e --- /dev/null +++ b/services/lookup/tests/main.rs @@ -0,0 +1,65 @@ +//! Lookup service end-to-end tests. + +use jade::testing::Jam; +use lookup::{ + SERVICE, + instruction::Instruction, + storage::{LookupStore, LookupTarget}, +}; + +const AUTHORIZER_ID: u32 = 500; +const SERVICE_ID: u32 = 601; +const SOURCE_ID: u32 = 602; + +#[test] +fn test_lookup_caches_preimages() { + jade::testing::util::init_logger(); + + let mut jam = Jam::default().with_auth(AUTHORIZER_ID, nauth::SERVICE.to_vec()); + jam.add_service(SERVICE_ID, SERVICE.to_vec()); + + let local_preimage = b"hello from lookup".to_vec(); + let local_hash = jam.add_preimage(SERVICE_ID, local_preimage.clone()); + + let external_preimage = b"external data blob".to_vec(); + let external_hash = jam.add_preimage(SOURCE_ID, external_preimage.clone()); + + let payload = codec::encode(&vec![ + Instruction::Lookup { hash: local_hash }, + Instruction::LookupFrom { + service: SOURCE_ID as u64, + hash: external_hash, + }, + ]) + .expect("failed to encode payload"); + + let info = jam + .execute(SERVICE_ID, payload) + .expect("failed to execute lookup request"); + + let store: LookupStore = info + .get_storage(SERVICE_ID, LookupStore::key()) + .expect("lookup store missing"); + + let local_target = LookupTarget { + service: SERVICE_ID as u64, + hash: local_hash, + }; + let external_target = LookupTarget { + service: SOURCE_ID as u64, + hash: external_hash, + }; + + assert_eq!( + store + .get_entry(&local_target) + .expect("local preimage missing"), + local_preimage.as_slice() + ); + assert_eq!( + store + .get_entry(&external_target) + .expect("external preimage missing"), + external_preimage.as_slice() + ); +}