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
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions crates/jade/src/host/general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Vec<u8>> {
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<Vec<u8>> {
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())
}
}
10 changes: 10 additions & 0 deletions crates/jade/src/host/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
24 changes: 24 additions & 0 deletions services/lookup/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
5 changes: 5 additions & 0 deletions services/lookup/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//! Build the service

fn main() {
cjam::build(env!("CARGO_PKG_NAME"), Some(cjam::ModuleType::Service)).ok();
}
13 changes: 13 additions & 0 deletions services/lookup/src/instruction.rs
Original file line number Diff line number Diff line change
@@ -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 },
}
12 changes: 12 additions & 0 deletions services/lookup/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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"));
121 changes: 121 additions & 0 deletions services/lookup/src/service.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
_package_hash: OpaqueHash,
) -> Vec<u8> {
if let Ok(instructions) = codec::decode::<Vec<Instruction>>(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<AccumulateItem>) -> Option<OpaqueHash> {
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::<Vec<Instruction>>(&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
}
65 changes: 65 additions & 0 deletions services/lookup/src/storage.rs
Original file line number Diff line number Diff line change
@@ -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<LookupTarget, Vec<u8>>,
}

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<u8>) {
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<LookupTarget, Vec<u8>> {
&self.entries
}

/// Storage key for the lookup store.
pub const fn key() -> &'static [u8] {
b"lookup::store"
}
}
65 changes: 65 additions & 0 deletions services/lookup/tests/main.rs
Original file line number Diff line number Diff line change
@@ -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()
);
}
Loading