use wasmparser::{Parser, Payload};
use crate::imp::compiler::error::{CompilerError, Result};
pub const MAX_MEMORY_PAGES: u64 = 6144;
#[allow(dead_code)] pub const NOMINAL_MEMORY_MIN_PAGES: u64 = 1;
pub const REQUIRED_HOST_IMPORTS: &[&str] = &[
"host_get_public_key",
"host_create_attestation",
"host_establish_session",
"host_verify_session",
"jwks_fetch",
"host_get_current_time",
"host_random_bytes",
"host_read_return_buffer",
];
pub const REQUIRED_EXPORTS: &[&str] = &[
"get_store_id",
"get_current_roothash",
"get_roothash_history",
"get_public_key",
"get_metadata",
"get_authentication_info",
"get_content",
"get_proof",
"alloc",
"dealloc",
"init",
"memory",
];
#[derive(Debug)]
pub struct Template {
pub bytes: Vec<u8>,
exports: Vec<String>,
imports: Vec<(String, String)>,
pub memory_min_pages: u64,
pub memory_max_pages: Option<u64>,
pub memory64: bool,
pub memory_shared: bool,
}
impl Template {
pub fn has_export(&self, name: &str) -> bool {
self.exports.iter().any(|e| e == name)
}
pub fn has_host_import(&self, name: &str) -> bool {
self.imports
.iter()
.any(|(m, n)| m == "dig_host" && n == name)
}
}
pub fn baked_template_bytes() -> &'static [u8] {
include_bytes!(concat!(env!("OUT_DIR"), "/digstore_guest_template.wasm"))
}
pub fn load_template(bytes: &[u8]) -> Result<Template> {
let mut exports = Vec::new();
let mut imports: Vec<(String, String)> = Vec::new();
let mut memory_min_pages: Option<u64> = None;
let mut memory_max_pages: Option<u64> = None;
let mut memory64 = false;
let mut memory_shared = false;
for payload in Parser::new(0).parse_all(bytes) {
let payload = payload.map_err(|e| CompilerError::InvalidTemplate(e.to_string()))?;
match payload {
Payload::ImportSection(reader) => {
for import in reader {
let import =
import.map_err(|e| CompilerError::InvalidTemplate(e.to_string()))?;
imports.push((import.module.to_string(), import.name.to_string()));
}
}
Payload::ExportSection(reader) => {
for export in reader {
let export =
export.map_err(|e| CompilerError::InvalidTemplate(e.to_string()))?;
exports.push(export.name.to_string());
}
}
Payload::MemorySection(reader) => {
for mem in reader {
let mem = mem.map_err(|e| CompilerError::InvalidTemplate(e.to_string()))?;
if memory_min_pages.is_none() {
memory_min_pages = Some(mem.initial);
memory_max_pages = mem.maximum;
memory64 = mem.memory64;
memory_shared = mem.shared;
}
}
}
_ => {}
}
}
for name in REQUIRED_EXPORTS {
if !exports.iter().any(|e| e == name) {
return Err(CompilerError::InvalidTemplate(format!(
"missing export {name}"
)));
}
}
let min = memory_min_pages
.ok_or_else(|| CompilerError::InvalidTemplate("template declares no memory".into()))?;
if memory64 {
return Err(CompilerError::InvalidTemplate(
"memory declares memory64 but §5.1 requires a 32-bit memory (memory64: false)".into(),
));
}
if memory_shared {
return Err(CompilerError::InvalidTemplate(
"memory declares shared but §5.1 requires an unshared memory (shared: false)".into(),
));
}
if let Some(max) = memory_max_pages {
if max > MAX_MEMORY_PAGES {
return Err(CompilerError::InvalidTemplate(format!(
"memory max {max} pages exceeds ceiling {MAX_MEMORY_PAGES}"
)));
}
}
Ok(Template {
bytes: bytes.to_vec(),
exports,
imports,
memory_min_pages: min,
memory_max_pages,
memory64,
memory_shared,
})
}
pub fn assert_memory_ceiling(module: &[u8]) -> Result<()> {
let t = load_template(module)?;
match t.memory_max_pages {
Some(max) if max == MAX_MEMORY_PAGES => Ok(()),
Some(max) => Err(CompilerError::Validation(format!(
"emitted module memory max {max} pages must equal §5.1 ceiling {MAX_MEMORY_PAGES} (384 MiB)"
))),
None => Err(CompilerError::Validation(format!(
"emitted module must declare memory maximum {MAX_MEMORY_PAGES} pages (§5.1 384 MiB ceiling)"
))),
}
}
pub fn assert_host_imports(module: &[u8]) -> Result<()> {
let t = load_template(module)?;
for name in REQUIRED_HOST_IMPORTS {
if !t.has_host_import(name) {
return Err(CompilerError::Validation(format!(
"emitted module missing §5.1 dig_host import {name}"
)));
}
}
Ok(())
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
#[test]
fn baked_template_has_all_required_exports() {
let bytes = baked_template_bytes();
let t = load_template(bytes).expect("template valid");
for name in REQUIRED_EXPORTS {
assert!(t.has_export(name), "missing export {name}");
}
}
#[test]
fn template_missing_export_is_rejected() {
let watsrc = r#"(module
(memory (export "memory") 1 256)
(func (export "get_store_id") (result i64) (i64.const 0))
(func (export "get_current_roothash") (result i64) (i64.const 0))
(func (export "get_roothash_history") (result i64) (i64.const 0))
(func (export "get_public_key") (result i64) (i64.const 0))
(func (export "get_metadata") (result i64) (i64.const 0))
(func (export "get_authentication_info") (result i64) (i64.const 0))
(func (export "get_proof") (param i32 i32) (result i64) (i64.const 0))
(func (export "alloc") (param i32) (result i32) (i32.const 0))
(func (export "dealloc") (param i32 i32))
(func (export "init") (result i32) (i32.const 0)))"#;
let bytes = wat::parse_str(watsrc).unwrap();
let err = load_template(&bytes).unwrap_err();
assert!(
err.to_string().contains("get_content"),
"unexpected error: {err}"
);
}
const FULL_ABI_FUNCS: &str = r#"
(func (export "get_store_id") (result i64) (i64.const 0))
(func (export "get_current_roothash") (result i64) (i64.const 0))
(func (export "get_roothash_history") (result i64) (i64.const 0))
(func (export "get_public_key") (result i64) (i64.const 0))
(func (export "get_metadata") (result i64) (i64.const 0))
(func (export "get_authentication_info") (result i64) (i64.const 0))
(func (export "get_content") (param i32 i32) (result i64) (i64.const 0))
(func (export "get_proof") (param i32 i32) (result i64) (i64.const 0))
(func (export "alloc") (param i32) (result i32) (i32.const 0))
(func (export "dealloc") (param i32 i32))
(func (export "init") (result i32) (i32.const 0)))"#;
fn full_abi_module(memory_decl: &str) -> Vec<u8> {
let src = format!("(module\n {memory_decl}\n{FULL_ABI_FUNCS}");
wat::parse_str(&src).unwrap()
}
#[test]
fn raw_template_without_declared_memory_max_is_accepted_by_load_template() {
let bytes = full_abi_module(r#"(memory (export "memory") 1)"#);
let t = load_template(&bytes).expect("raw template valid");
assert_eq!(t.memory_max_pages, None);
}
#[test]
fn emitted_module_without_declared_memory_max_fails_ceiling_check() {
let bytes = full_abi_module(r#"(memory (export "memory") 1)"#);
let err = assert_memory_ceiling(&bytes).unwrap_err();
assert!(
err.to_string().contains("must declare memory maximum"),
"unexpected error: {err}"
);
}
#[test]
fn emitted_module_with_memory_max_under_ceiling_fails_ceiling_check() {
let bytes = full_abi_module(r#"(memory (export "memory") 1 128)"#);
let err = assert_memory_ceiling(&bytes).unwrap_err();
assert!(
err.to_string().contains("must equal §5.1 ceiling"),
"unexpected error: {err}"
);
}
#[test]
fn emitted_module_with_memory_max_exactly_ceiling_passes() {
let bytes = full_abi_module(r#"(memory (export "memory") 1 6144)"#);
assert_memory_ceiling(&bytes).expect("6144 is the ceiling");
let t = load_template(baked_template_bytes()).expect("baked template valid");
assert_eq!(t.memory_max_pages, Some(MAX_MEMORY_PAGES));
}
#[test]
fn template_declaring_memory64_is_rejected() {
let bytes = full_abi_module(r#"(memory (export "memory") i64 1 256)"#);
let err = load_template(&bytes).unwrap_err();
assert!(
err.to_string().contains("memory64"),
"unexpected error: {err}"
);
}
#[test]
fn template_declaring_shared_memory_is_rejected() {
let bytes = full_abi_module(r#"(memory (export "memory") 1 256 shared)"#);
let err = load_template(&bytes).unwrap_err();
assert!(
err.to_string().contains("shared"),
"unexpected error: {err}"
);
}
#[test]
fn baked_template_declares_nominal_memory_min_of_one_page() {
let t = load_template(baked_template_bytes()).expect("baked template valid");
assert_eq!(
t.memory_min_pages, NOMINAL_MEMORY_MIN_PAGES,
"§5.1 nominal MemoryType.minimum is {NOMINAL_MEMORY_MIN_PAGES} page; \
baked template declares {}",
t.memory_min_pages
);
}
#[test]
fn template_with_memory_max_over_ceiling_is_rejected() {
let watsrc = r#"(module
(memory (export "memory") 1 6145)
(func (export "get_store_id") (result i64) (i64.const 0))
(func (export "get_current_roothash") (result i64) (i64.const 0))
(func (export "get_roothash_history") (result i64) (i64.const 0))
(func (export "get_public_key") (result i64) (i64.const 0))
(func (export "get_metadata") (result i64) (i64.const 0))
(func (export "get_authentication_info") (result i64) (i64.const 0))
(func (export "get_content") (param i32 i32) (result i64) (i64.const 0))
(func (export "get_proof") (param i32 i32) (result i64) (i64.const 0))
(func (export "alloc") (param i32) (result i32) (i32.const 0))
(func (export "dealloc") (param i32 i32))
(func (export "init") (result i32) (i32.const 0)))"#;
let bytes = wat::parse_str(watsrc).unwrap();
let err = load_template(&bytes).unwrap_err();
assert!(err.to_string().contains("exceeds ceiling"));
}
}