Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-product-wasm/src/lib.rs

//! **The guest host** — a signed `.wasm` behind the same
//! [`monetize_product::ProductFactory`] a linked plugin registers through.
//!
//! ```text
//!  [products.<id>] guest = "<id>.wasm"    ──▶ factory(id).build(&ProductBuild)
//!       guest_digest / guest_trust_roots  ──▶ refuse by name unless the bytes are vouched for
//!       grants = ["grpc", …]              ──▶ the linker defines exactly those doors
//!                                            ▼
//!                             GuestProduct: Box<dyn Product>
//!                             every verb = one plugin_call(op, JSON) under fuel + a deadline
//! ```
//!
//! What the server sees is a `Box<dyn Product>`; what the operator sees is a
//! `.wasm` and a `.p7s` beside it. What the guest sees is
//! [`monetize_product_guest`]'s doors, and only the granted ones.
//!
//! # Load order, and why
//!
//! 1. The block must name a guest, and **somebody must vouch for the bytes**:
//!    a pinned digest, trust roots, or both. Neither is refused by name — a
//!    file in a directory is not a reason to run it inside the server.
//! 2. The digest, over the bytes on disk. A mismatch names both digests.
//! 3. The signature (`guest_host::verify`), BEFORE compilation.
//! 4. The limits: the product kind's ceilings, narrowed by the block.
//! 5. Compile; the export set; no import outside `env`; every import in the
//!    vocabulary and unlocked by a grant — an ungranted import is NOT DEFINED
//!    on the linker, so the module could not instantiate, and it is said so
//!    here rather than at the first poll.
//! 6. `plugin_abi` negotiated, `plugin_manifest` read: the manifest's product
//!    must be this block's id, its grants must be granted (gate A), and every
//!    real import must be one the manifest declared (gate B).
//! 7. `id` and `meters`, once, leaked into the `&'static` shapes the trait
//!    answers — one bounded allocation per product per boot.
//!
//! # What is deliberately not here
//!
//! Prices, the ledger, the transaction, the poller, the console: untouched, and
//! they call `dyn Product` as they always did. The appliance grow verbs are
//! poll-style, as the design note describes. (The restore and twinship lanes are
//! retired: a repair is an order the estate pulls, never a call into a box.)


use monetize_product::{Product, ProductBuild, ProductFactory};

pub mod doors;
pub mod hot;
pub mod load;
pub mod product;
pub mod secrets;
pub mod signup;
pub mod vendor;

pub use load::{identify, Block, LoadedGuest, PRODUCT_CALL_TIMEOUT, PRODUCT_FUEL_CEILING};
pub use hot::{block_fingerprint, HotGuest, Replaced};
pub use product::GuestProduct;
pub use vendor::{GuestVendor, HotVendor, VendorGuestBuild, VendorReplaced};

/// Everything a guest must export — `guest_host`'s convention, unchanged.
pub use guest_host::exports;

/// **The digest a `guest_digest =` key pins**, over the module bytes. Whoever
/// pins one has to compute it the same way the gate does, so the function is
/// re-exported here rather than left as a second implementation in a deploy
/// script.
pub use guest_host::digest_hex;

/// **The factory for `[products.<id>] guest = …`.** `id` is the block's id;
/// the server leaks it once per configured guest ([`leak_id`]) because a
/// factory's name is `&'static` and a config file is not.
pub fn factory(id: &'static str) -> ProductFactory {
    ProductFactory { name: id, build: build_guest, mint_identity: mint_guest_identity, guardian: None }
}

/// A configured id as the `&'static str` a factory name is. Interned: the same
/// name is leaked ONCE per process however many times a guest is reloaded.
pub fn leak_id(id: &str) -> &'static str {
    intern(id)
}

/// **A runtime id as the `&'static str` the traits answer** (`Product::id`,
/// `Vendor::name`). One leak per DISTINCT name for the life of the
/// process — ids come from the config file, so the set is as large as the
/// operator's list and a hot reload of the same id costs nothing.
pub fn intern(id: &str) -> &'static str {
    static NAMES: std::sync::Mutex<std::collections::BTreeSet<&'static str>> = std::sync::Mutex::new(std::collections::BTreeSet::new());
    let mut names = NAMES.lock().unwrap_or_else(|p| p.into_inner());
    if let Some(n) = names.get(id) {
        return n;
    }
    let n: &'static str = Box::leak(id.to_string().into_boxed_str());
    names.insert(n);
    n
}

/// A vendor's market list as the `&'static [Market]` the trait answers, one
/// leak per distinct list (two markets: at most three lists, ever).
pub fn intern_markets(markets: &[monetize_pay::Market]) -> &'static [monetize_pay::Market] {
    static LISTS: Pool<monetize_pay::Market> = std::sync::Mutex::new(Vec::new());
    intern_slice(&LISTS, markets)
}

type Pool<T> = std::sync::Mutex<Vec<&'static [T]>>;

/// **A table as the `&'static` slice a trait answers**, held once per distinct
/// value: a hot reload that brings the same table back costs nothing, so the
/// leak is bounded by the distinct tables the process ever sees, not by how
/// often it reloads.
pub(crate) fn intern_slice<T: Clone + PartialEq + Send + Sync + 'static>(pool: &'static Pool<T>, v: &[T]) -> &'static [T] {
    let mut p = pool.lock().unwrap_or_else(|e| e.into_inner());
    if let Some(found) = p.iter().find(|s| **s == v) {
        return found;
    }
    let leaked: &'static [T] = Box::leak(v.to_vec().into_boxed_slice());
    p.push(leaked);
    leaked
}

fn build_guest(b: &ProductBuild<'_>) -> Result<Box<dyn Product>, String> {
    Ok(Box::new(GuestProduct::build(b)?))
}

/// **A guest product has no identity to mint** (monetize v2.0 P2 T13, A28 M3): the host's
/// signing door and its identity key are gone, for every kind of guest. Refused by name.
fn mint_guest_identity(b: &ProductBuild<'_>) -> Result<String, String> {
    Err(b.err("a guest product has no identity to mint: the host mints none since monetize v2.0 T13 (the six-op product dials nothing)"))
}