diff --git a/core/indexer/src/database/native_contracts.rs b/core/indexer/src/database/native_contracts.rs index 5d96877f..6d0e9474 100644 --- a/core/indexer/src/database/native_contracts.rs +++ b/core/indexer/src/database/native_contracts.rs @@ -5,6 +5,8 @@ pub const STAKING: &[u8] = include_bytes!("../../../../native-contracts/binaries pub const REGISTRY: &[u8] = include_bytes!("../../../../native-contracts/binaries/registry.wasm.br"); pub const NFT: &[u8] = include_bytes!("../../../../native-contracts/binaries/nft.wasm.br"); +pub const CONGESTION: &[u8] = + include_bytes!("../../../../native-contracts/binaries/congestion.wasm.br"); /// Ordered list of native contracts published at genesis. Publish order /// determines contract IDs (1-indexed, assigned in iteration order), so @@ -17,4 +19,5 @@ pub const NATIVE_CONTRACTS: &[(&str, &[u8])] = &[ ("staking", STAKING), ("registry", REGISTRY), ("nft", NFT), + ("congestion", CONGESTION), ]; diff --git a/core/indexer/src/runtime/congestion.rs b/core/indexer/src/runtime/congestion.rs new file mode 100644 index 00000000..3c521720 --- /dev/null +++ b/core/indexer/src/runtime/congestion.rs @@ -0,0 +1,19 @@ +use crate::runtime::Runtime; +use crate::testlib_exports::*; + +import!( + name = "congestion", + mod_name = "api", + height = 0, + tx_index = 0, + path = "../../native-contracts/congestion/wit", + public = true, +); + +pub fn address() -> ContractAddress { + ContractAddress { + name: "congestion".to_string(), + height: 0, + tx_index: 0, + } +} diff --git a/core/indexer/src/runtime/mod.rs b/core/indexer/src/runtime/mod.rs index dfbe6c0f..e1de1204 100644 --- a/core/indexer/src/runtime/mod.rs +++ b/core/indexer/src/runtime/mod.rs @@ -1,6 +1,7 @@ extern crate alloc; mod component_cache; +pub mod congestion; pub mod counter; pub mod file_ledger; pub mod filestorage; diff --git a/core/indexer/tests/contracts/congestion.rs b/core/indexer/tests/contracts/congestion.rs new file mode 100644 index 00000000..11e0e22a --- /dev/null +++ b/core/indexer/tests/contracts/congestion.rs @@ -0,0 +1,163 @@ +//! Lite (in-process) tests for the congestion-pricing contract's β(t) +//! recurrence. Validates the deterministic state machine against the reference +//! values from `Documentation/modeling/kontor_v1/congestion.py`. + +use anyhow::{Result, anyhow}; +use indexer::runtime::Decimal; +use indexer::runtime::congestion::api as congestion; +use indexer::test_utils::test_runtime_with_genesis; +use testlib::Signer; + +fn dec(n: u64) -> Decimal { + Decimal::try_from(n).unwrap() +} + +/// `n/d` as an exact fixed-point Decimal (utilizations and β values are +/// fractional). Base-10 fractions like 9/10 are exact in the fixed-point repr. +fn frac(n: u64, d: u64) -> Decimal { + dec(n) / dec(d) +} + +fn core() -> Signer { + Signer::Core(Box::new(Signer::Nobody)) +} + +#[tokio::test] +async fn congestion_beta_recurrence() -> Result<()> { + let (mut rt, _dir, _name) = test_runtime_with_genesis(&[]).await?; + + // Defaults (economics spec §Congestion Pricing). + let p = congestion::get_congestion_params(&mut rt).await?; + assert_eq!(p.u_low_bps, 2_000); + assert_eq!(p.u_high_bps, 8_000); + assert_eq!(p.lambda_decay_bps, 9_500); + assert_eq!(p.kappa_cong_bps, 20_000); + + // β starts at 0 — at genesis utilization is low, fees ≈ 0. + assert_eq!(congestion::get_beta(&mut rt).await?, dec(0)); + + // Region 3 (u = 0.90 ≥ u_high): β = max(0,1)·(1 + 2·(0.9−0.8)) = 1.2. + let b = congestion::update_beta(&mut rt, &core(), frac(9, 10)) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + assert_eq!(b, frac(12, 10), "first high-util step → 1.2"); + + // Sustained high util compounds: max(1.2,1)·1.2 = 1.44. + let b = congestion::update_beta(&mut rt, &core(), frac(9, 10)) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + assert_eq!(b, frac(144, 100), "compounding → 1.44"); + + // Region 1 (u = 0.10 < u_low, free): β = 1.44·0.95 = 1.368. + let b = congestion::update_beta(&mut rt, &core(), frac(1, 10)) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + assert_eq!(b, frac(1368, 1000), "low-util decay → 1.368"); + + // Fee multiplier = φ_base · β. + let m = congestion::fee_multiplier(&mut rt) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + assert_eq!( + m, + p.phi_base * frac(1368, 1000), + "fee multiplier = φ_base·β" + ); + + Ok(()) +} + +#[tokio::test] +async fn congestion_smoothstep_band_and_decay_floor() -> Result<()> { + let (mut rt, _dir, _name) = test_runtime_with_genesis(&[]).await?; + + // Region 2 (u = 0.50, mid-band): x = (0.5−0.2)/(0.8−0.2) = 0.5; + // S(x) = 3·0.25 − 2·0.125 = 0.5; max(S, 0·0.95) = 0.5. + let b = congestion::update_beta(&mut rt, &core(), frac(5, 10)) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + assert_eq!(b, frac(5, 10), "smoothstep at midpoint → 0.5"); + + // Staying low decays toward 0: 0.5·0.95 = 0.475. + let b = congestion::update_beta(&mut rt, &core(), frac(1, 10)) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + assert_eq!(b, frac(475, 1000), "decay → 0.475"); + + Ok(()) +} + +/// β(t) region/bound invariants over a deterministic random walk of +/// utilizations spanning all three regions. Checks the structural properties +/// (robust to fixed-point rounding) rather than re-deriving exact values: +/// region 1 decays (β'≤β), region 3 grows (β'≥max(β,1)), region 2 is bounded by +/// the smoothstep ceiling (β'≤max(β,1)), β never goes negative, and the fee +/// multiplier always equals φ_base·β. +#[tokio::test] +async fn congestion_beta_invariants_over_random_walk() -> Result<()> { + let (mut rt, _dir, _name) = test_runtime_with_genesis(&[]).await?; + let p = congestion::get_congestion_params(&mut rt).await?; + let one = dec(1); + let zero = dec(0); + + let mut prev = congestion::get_beta(&mut rt).await?; + for seed in 0u64..60 { + let u_bps = seed.wrapping_mul(167).wrapping_add(13) % 10_001; // 0..=10000 + let u = frac(u_bps, 10_000); + + let b = congestion::update_beta(&mut rt, &core(), u) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + + assert!(b >= zero, "seed {seed}: β never negative"); + if u_bps < p.u_low_bps { + assert!(b <= prev, "seed {seed}: region 1 decays (β' ≤ β)"); + } else if u_bps > p.u_high_bps { + assert!(b >= one, "seed {seed}: region 3 β' ≥ 1"); + assert!(b >= prev, "seed {seed}: region 3 grows (β' ≥ β)"); + } else { + let ceil = if prev > one { prev } else { one }; + assert!( + b <= ceil, + "seed {seed}: region 2 β' ≤ max(β,1) (smoothstep ≤ 1)" + ); + } + + let m = congestion::fee_multiplier(&mut rt) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + assert_eq!(m, p.phi_base * b, "seed {seed}: fee multiplier = φ_base·β"); + + prev = b; + } + Ok(()) +} + +/// Smoothstep is continuous with the neighbouring regions at the band edges: +/// at u_high, x=1 ⇒ S(1)=1 (joins region 3's floor); at u_low, x=0 ⇒ S(0)=0, +/// so β' is the region-1 decay floor. +#[tokio::test] +async fn congestion_smoothstep_continuous_at_band_edges() -> Result<()> { + let (mut rt, _dir, _name) = test_runtime_with_genesis(&[]).await?; + let p = congestion::get_congestion_params(&mut rt).await?; + let u_low = frac(p.u_low_bps, 10_000); // 0.20 + let u_high = frac(p.u_high_bps, 10_000); // 0.80 + + // Upper edge: from β=0, S(1)=1 ⇒ max(1, 0·0.95) = 1. + let b = congestion::update_beta(&mut rt, &core(), u_high) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + assert_eq!(b, dec(1), "smoothstep top S(1)=1 at u_high"); + + // Lower edge: now β=1, S(0)=0 ⇒ max(0, 1·0.95) = 0.95 (decay floor). + let b = congestion::update_beta(&mut rt, &core(), u_low) + .await? + .map_err(|e| anyhow!("{e:?}"))?; + assert_eq!( + b, + frac(95, 100), + "smoothstep bottom S(0)=0, floored by decay 0.95" + ); + + Ok(()) +} diff --git a/core/indexer/tests/contracts/mod.rs b/core/indexer/tests/contracts/mod.rs index c3773f00..5701a755 100644 --- a/core/indexer/tests/contracts/mod.rs +++ b/core/indexer/tests/contracts/mod.rs @@ -6,6 +6,7 @@ mod bls_publisher_pays; mod bls_replay_protection; mod bls_user_registry; mod compose; +mod congestion; mod counter_contract; mod crypto_contract; mod error_classification; diff --git a/native-contracts/Cargo.lock b/native-contracts/Cargo.lock index 7203a7fc..352090ad 100644 --- a/native-contracts/Cargo.lock +++ b/native-contracts/Cargo.lock @@ -20,6 +20,13 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "congestion" +version = "0.1.0" +dependencies = [ + "stdlib", +] + [[package]] name = "darling" version = "0.23.0" diff --git a/native-contracts/Cargo.toml b/native-contracts/Cargo.toml index 42dba87e..af0a4ed2 100644 --- a/native-contracts/Cargo.toml +++ b/native-contracts/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["token", "filestorage", "staking", "registry", "nft"] +members = ["token", "filestorage", "staking", "registry", "nft", "congestion"] resolver = "2" [profile.release] diff --git a/native-contracts/binaries/congestion.wasm.br b/native-contracts/binaries/congestion.wasm.br new file mode 100644 index 00000000..48b0904d Binary files /dev/null and b/native-contracts/binaries/congestion.wasm.br differ diff --git a/native-contracts/congestion/Cargo.toml b/native-contracts/congestion/Cargo.toml new file mode 100644 index 00000000..2dedc9f6 --- /dev/null +++ b/native-contracts/congestion/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "congestion" +version = "0.1.0" +edition = "2024" + +[lints] +workspace = true + +[dependencies] +stdlib = { workspace = true } + +[lib] +crate-type = ["cdylib"] diff --git a/native-contracts/congestion/src/lib.rs b/native-contracts/congestion/src/lib.rs new file mode 100644 index 00000000..28e1d2c1 --- /dev/null +++ b/native-contracts/congestion/src/lib.rs @@ -0,0 +1,151 @@ +#![no_std] +contract!(name = "congestion"); + +use stdlib::*; + +// ───────────────────────────────────────────────────────────────── +// Congestion-pricing parameters (economics spec §Congestion Pricing). +// β(t) = ⎧ β(t-1)·λ_decay if u < u_low +// ⎨ max(S(x), β(t-1)·λ_decay) if u_low ≤ u < u_high +// ⎩ max(β(t-1), 1)·(1 + κ_cong·(u - u_high)) if u ≥ u_high +// x = (u - u_low)/(u_high - u_low), S(x) = 3x² - 2x³ (smoothstep) +// Fee = φ_base · β +// ───────────────────────────────────────────────────────────────── + +const BPS_DENOM: u64 = 10_000; + +/// Lower utilization threshold (β decays toward 0 below it). 20%. +const DEFAULT_U_LOW_BPS: u64 = 2_000; +/// Upper utilization threshold (β grows multiplicatively above it). 80%. +const DEFAULT_U_HIGH_BPS: u64 = 8_000; +/// Per-block decay factor when u < u_low. 0.95. +const DEFAULT_LAMBDA_DECAY_BPS: u64 = 9_500; +/// Congestion growth rate above u_high. 2.0. +const DEFAULT_KAPPA_CONG_BPS: u64 = 20_000; +/// Smart-contract base fee φ_base (KOR per gas unit), calibrated in +/// Documentation `modeling/analyses/12`. 2.5e-7. +const DEFAULT_PHI_BASE: &str = "0.00000025"; + +#[derive(Clone, Default, StorageRoot)] +struct CongestionState { + /// Current congestion multiplier β(t). + pub beta: Decimal, + pub u_low_bps: u64, + pub u_high_bps: u64, + pub lambda_decay_bps: u64, + pub kappa_cong_bps: u64, + /// φ_base, the KOR-per-gas numeraire (fixed-point, not bps). + pub phi_base: Decimal, +} + +impl Guest for Congestion { + fn init(ctx: &ProcContext) -> Contract { + let zero: Decimal = 0u64.try_into().expect("zero fits in Decimal"); + let phi_base: Decimal = DEFAULT_PHI_BASE.into(); + CongestionState { + // β starts at 0: at genesis utilization is low, fees ≈ 0. + beta: zero, + u_low_bps: DEFAULT_U_LOW_BPS, + u_high_bps: DEFAULT_U_HIGH_BPS, + lambda_decay_bps: DEFAULT_LAMBDA_DECAY_BPS, + kappa_cong_bps: DEFAULT_KAPPA_CONG_BPS, + phi_base, + } + .init(ctx); + ctx.contract() + } + + /// Advance β from the prior block's `utilization` (a fraction in [0,1]). + /// Core-context: the reactor calls this once per block. + fn update_beta(ctx: &CoreContext, utilization: Decimal) -> Result { + let model = ctx.proc_context().model(); + let denom: Decimal = BPS_DENOM.try_into()?; + let u_low = bps_to_decimal(model.u_low_bps(), denom)?; + let u_high = bps_to_decimal(model.u_high_bps(), denom)?; + let lambda_decay = bps_to_decimal(model.lambda_decay_bps(), denom)?; + let kappa_cong = bps_to_decimal(model.kappa_cong_bps(), denom)?; + + let beta = beta_step( + model.beta(), + utilization, + u_low, + u_high, + lambda_decay, + kappa_cong, + )?; + model.set_beta(beta); + Ok(beta) + } + + fn get_beta(ctx: &ViewContext) -> Decimal { + ctx.model().beta() + } + + /// φ_base · β — the gas-fee multiplier the runtime applies per unit of gas. + fn fee_multiplier(ctx: &ViewContext) -> Result { + let model = ctx.model(); + model.phi_base().mul(model.beta()) + } + + fn get_congestion_params(ctx: &ViewContext) -> CongestionParams { + let model = ctx.model(); + CongestionParams { + u_low_bps: model.u_low_bps(), + u_high_bps: model.u_high_bps(), + lambda_decay_bps: model.lambda_decay_bps(), + kappa_cong_bps: model.kappa_cong_bps(), + phi_base: model.phi_base(), + } + } + + fn set_congestion_params(ctx: &CoreContext, params: CongestionParams) -> Result<(), Error> { + if params.u_low_bps >= params.u_high_bps { + return Err(Error::Message("u_low_bps must be < u_high_bps".to_string())); + } + if params.u_high_bps > BPS_DENOM { + return Err(Error::Message("u_high_bps must be <= 10000".to_string())); + } + let model = ctx.proc_context().model(); + model.set_u_low_bps(params.u_low_bps); + model.set_u_high_bps(params.u_high_bps); + model.set_lambda_decay_bps(params.lambda_decay_bps); + model.set_kappa_cong_bps(params.kappa_cong_bps); + model.set_phi_base(params.phi_base); + Ok(()) + } +} + +fn bps_to_decimal(bps: u64, denom: Decimal) -> Result { + let v: Decimal = bps.try_into()?; + v.div(denom) +} + +/// One step of the β(t) recurrence. Pure deterministic fixed-point math. +fn beta_step( + beta_prev: Decimal, + u: Decimal, + u_low: Decimal, + u_high: Decimal, + lambda_decay: Decimal, + kappa_cong: Decimal, +) -> Result { + if u < u_low { + // Region 1 (free): decay toward 0. + return beta_prev.mul(lambda_decay); + } + if u < u_high { + // Region 2 (smoothstep ramp): max(S(x), decayed). + // x = (u - u_low)/(u_high - u_low) ∈ [0,1); S(x) = 3x² - 2x³ = x²(3 - 2x). + let x = u.sub(u_low)?.div(u_high.sub(u_low)?)?; + let three: Decimal = 3u64.try_into()?; + let two: Decimal = 2u64.try_into()?; + let s = x.mul(x)?.mul(three.sub(two.mul(x)?)?)?; + let decayed = beta_prev.mul(lambda_decay)?; + return Ok(if s > decayed { s } else { decayed }); + } + // Region 3 (multiplicative growth): max(β, 1)·(1 + κ·(u - u_high)). + let one: Decimal = 1u64.try_into()?; + let base = if beta_prev > one { beta_prev } else { one }; + let growth = one.add(kappa_cong.mul(u.sub(u_high)?)?)?; + base.mul(growth) +} diff --git a/native-contracts/congestion/wit/contract.wit b/native-contracts/congestion/wit/contract.wit new file mode 100644 index 00000000..ad205ed0 --- /dev/null +++ b/native-contracts/congestion/wit/contract.wit @@ -0,0 +1,29 @@ +package root:component; + +world root { + include kontor:built-in/built-in; + use kontor:built-in/context.{view-context, proc-context, core-context, contract}; + use kontor:built-in/error.{error}; + use kontor:built-in/numbers.{decimal}; + + // Congestion-pricing parameters (economics spec §Congestion Pricing). + record congestion-params { + u-low-bps: u64, + u-high-bps: u64, + lambda-decay-bps: u64, + kappa-cong-bps: u64, + phi-base: decimal, + } + + export init: async func(ctx: borrow) -> contract; + + // Advance β(t) from the prior block's utilization (fraction in [0,1]). + // Core-context: the reactor calls this once per block. Returns the new β. + export update-beta: async func(ctx: borrow, utilization: decimal) -> result; + + export get-beta: async func(ctx: borrow) -> decimal; + // Current gas-fee multiplier: φ_base · β (KOR per gas unit). + export fee-multiplier: async func(ctx: borrow) -> result; + export get-congestion-params: async func(ctx: borrow) -> congestion-params; + export set-congestion-params: async func(ctx: borrow, params: congestion-params) -> result<_, error>; +} diff --git a/native-contracts/congestion/wit/deps/built-in.wit b/native-contracts/congestion/wit/deps/built-in.wit new file mode 100644 index 00000000..765e4ee6 --- /dev/null +++ b/native-contracts/congestion/wit/deps/built-in.wit @@ -0,0 +1,316 @@ +package kontor:built-in; + +interface context { + use error.{error}; + + /// Freely-constructible record naming a contract — used as the + /// target for cross-contract calls (`foreign.call`). The passive + /// side of the contract-identity pair (see `contract` resource). + record contract-address { + name: string, + height: u64, + tx-index: u32, + } + + /// Active proof of "I am the contract at this address." Same + /// HolderRef/Holder split: `contract-address` is the freely- + /// constructible record (anyone can pass one around); `contract` + /// is the host-issued resource that can only be obtained from the + /// host via `proc-context.contract` / `core-context.contract` / + /// `view-context.contract`. + /// + /// Returning a `contract` from `init` is how a contract proves + /// which address it is — there's no way to fabricate one pointing + /// at another contract. Once a `contract` crosses the result-row + /// boundary the host drains it to a `contract-address` record for + /// WAVE serialization; integrity is in-execution only. + resource contract { + address: async func() -> contract-address; + } + + variant holder-ref { + x-only-pubkey(string), + signer-id(u64), + core, + burner, + utxo(out-point), + } + + resource holder { + key: async func() -> string; + from-ref: static async func(ref: holder-ref) -> result; + as-ref: async func() -> holder-ref; + } + + resource signer { + key: async func() -> string; + as-holder: async func() -> holder; + as-ref: async func() -> holder-ref; + } + + variant signer-ref { + signer-id(u64), + x-only-pubkey(string), + } + + record out-point { + txid: string, + vout: u32, + } + + resource transaction { + id: async func() -> string; + out-point: async func() -> out-point; + op-return-data: async func() -> option; + } + + resource keys { + next: async func() -> option; + } + + resource view-storage { + get-str: async func(path: string) -> option; + get-u64: async func(path: string) -> option; + get-s64: async func(path: string) -> option; + get-bool: async func(path: string) -> option; + get-list-u8: async func(path: string) -> option>; + get-keys: async func(path: string) -> keys; + exists: async func(path: string) -> bool; + extend-path-with-match: async func(path: string, variants: list) -> option; + } + + resource proc-storage { + get-str: async func(path: string) -> option; + get-u64: async func(path: string) -> option; + get-s64: async func(path: string) -> option; + get-bool: async func(path: string) -> option; + get-list-u8: async func(path: string) -> option>; + get-keys: async func(path: string) -> keys; + exists: async func(path: string) -> bool; + extend-path-with-match: async func(path: string, variants: list) -> option; + + set-str: async func(path: string, value: string); + set-u64: async func(path: string, value: u64); + set-s64: async func(path: string, value: s64); + set-bool: async func(path: string, value: bool); + set-list-u8: async func(path: string, value: list); + set-void: async func(path: string); + delete-matching-paths: async func(base-path: string, variants: list) -> u64; + view-storage: async func() -> view-storage; + } + + resource view-context { + storage: async func() -> view-storage; + /// See `proc-context.contract`. View functions may also need + /// to know which contract they're running in (e.g. for self- + /// referential lookups). + contract: async func() -> contract; + } + + resource proc-context { + signer: async func() -> signer; + /// Who pays this op's gas. A `holder` (not a `signer`) by design — + /// contracts can credit the payer's balance but cannot spend on + /// their behalf, since the payer only consented to pay this op's + /// gas. Equals `signer.as-holder()` by default; redirected by a + /// previous-input `Sponsor` Inst (direct context) or by + /// `AggregateSigner.sponsored = true` (aggregate context). + payer: async func() -> holder; + contract-signer: async func() -> signer; + view-context: async func() -> view-context; + generate-id: async func() -> string; + storage: async func() -> proc-storage; + transaction: async func() -> transaction; + block-height: async func() -> u64; + /// The executing contract's own identity as a `contract` resource — + /// the only way for the contract to obtain one pointing at itself. + /// Returned from `init` so the new contract's address surfaces on + /// the publish op's result row; usable by any proc-context method + /// that needs to assert "I am the contract at
." + contract: async func() -> contract; + } + + resource fall-context { + signer: async func() -> option; + /// See `proc-context.payer`. `none` when `signer` is also `none` + /// (a fall context with no acting signer has no payer either). + payer: async func() -> option; + proc-context: async func() -> option; + view-context: async func() -> view-context; + } + + resource core-context { + proc-context: async func() -> proc-context; + signer-proc-context: async func() -> proc-context; + core-signer: async func() -> signer; + /// See `proc-context.contract`. Native contracts running as core + /// also need an integrity-bound handle to their own identity. + contract: async func() -> contract; + } +} + +interface foreign { + use context.{signer, contract-address}; + + call: async func(signer: option, contract-address: contract-address, expr: string) -> string; +} + +interface crypto { + hash: async func(input: string) -> tuple>; + hash-with-salt: async func(input: string, salt: string) -> tuple>; + + hkdf-derive: async func( + ikm: list, + salt: list, + info: list + ) -> list; +} + +interface error { + variant error { + message(string), + overflow(string), + div-by-zero(string), + syntax(string), + validation(string), + } +} + +interface numbers { + use error.{error}; + + enum sign { + plus, + minus + } + + record integer { + r0: u64, + r1: u64, + r2: u64, + r3: u64, + sign: sign + } + + record decimal { + r0: u64, + r1: u64, + r2: u64, + r3: u64, + sign: sign + } + + enum ordering { + less, + equal, + greater + } + + u64-to-integer: async func(i: u64) -> integer; + s64-to-integer: async func(i: s64) -> integer; + string-to-integer: async func(s: string) -> result; + integer-to-string: async func(i: integer) -> string; + eq-integer: async func(a: integer, b: integer) -> bool; + cmp-integer: async func(a: integer, b: integer) -> ordering; + add-integer: async func(a: integer, b: integer) -> result; + sub-integer: async func(a: integer, b: integer) -> result; + mul-integer: async func(a: integer, b: integer) -> result; + div-integer: async func(a: integer, b: integer) -> result; + sqrt-integer: async func(i: integer) -> result; + + integer-to-decimal: async func(i: integer) -> result; + decimal-to-integer: async func(d: decimal) -> result; + u64-to-decimal: async func(i: u64) -> result; + s64-to-decimal: async func(i: s64) -> result; + f64-to-decimal: async func(f: f64) -> result; + string-to-decimal: async func(s: string) -> result; + decimal-to-string: async func(d: decimal) -> string; + eq-decimal: async func(a: decimal, b: decimal) -> result; + cmp-decimal: async func(a: decimal, b: decimal) -> ordering; + add-decimal: async func(a: decimal, b: decimal) -> result; + sub-decimal: async func(a: decimal, b: decimal) -> result; + mul-decimal: async func(a: decimal, b: decimal) -> result; + div-decimal: async func(a: decimal, b: decimal) -> result; + log10-decimal: async func(a: decimal) -> result; +} + +interface file-registry { + use error.{error}; + + record raw-file-descriptor { + file-id: string, + object-id: string, + nonce: list, + root: list, + padded-len: u64, + original-size: u64, + filename: string, + } + + resource file-descriptor { + file-id: async func() -> string; + from-raw: static async func(raw: raw-file-descriptor) -> result; + compute-challenge-id: async func( + block-height: u64, + num-challenges: u64, + seed: list, + prover-id: string + ) -> result; + } + + add-file: async func(file-descriptor: borrow); + + get-file-descriptor: async func(file-id: string) -> option; + + // ───────────────────────────────────────────────────────────────── + // Proof Verification + // ───────────────────────────────────────────────────────────────── + + record challenge-input { + challenge-id: string, + file-id: string, + block-height: u64, + num-challenges: u64, + seed: list, + prover-id: string, + } + + enum verify-result { + verified, + rejected, + invalid, + } + + resource proof { + from-bytes: static async func(bytes: list) -> result; + challenge-ids: async func() -> list; + verify: async func(challenges: list) -> result; + } +} + +interface testing { + /// Always returns an error. Used to test host function error propagation. + host-error: async func() -> string; + /// Always panics. Used to test host function panic propagation. + host-panic: async func() -> string; +} + +interface registry { + /// Consume fuel for a RegisterBlsKey operation. The actual verification + /// and DB write happen in the reactor (outside the contract's fuel + /// scope); this call meters the caller for the operation so the fuel + /// accounting matches reality. Traps on insufficient fuel — aborts + /// the enclosing contract call. + register-bls-key: async func(); +} + +world built-in { + import context; + import foreign; + import crypto; + import error; + import numbers; + import file-registry; + import testing; + import registry; +}