Skip to content

Instantly share code, notes, and snippets.

@roninjin10
Last active November 8, 2025 01:34
Show Gist options
  • Select an option

  • Save roninjin10/ad52b781bf3366c9968822f796401b6c to your computer and use it in GitHub Desktop.

Select an option

Save roninjin10/ad52b781bf3366c9968822f796401b6c to your computer and use it in GitHub Desktop.
App layer ephemeral channels

EVENT‑SOURCED STATE CHANNELS: PRODUCT REQUIREMENTS DOCUMENT (v0.0.4)

Version: 0.0.4 Date: 2025‑11‑08 Status: Draft Authors: Fucory


Table of Contents

  1. Executive Summary
  2. Problem Statement
  3. Solution Overview
  4. Core Concepts
  5. System Architecture
  6. Detailed Requirements
  7. Protocol Specifications
  8. Worked Examples
  9. Implementation Roadmap
  10. Technical Decisions & Rationale
  11. Security Considerations
  12. Performance Targets
  13. Out of Scope
  14. Open Questions
  15. References & Prior Art

1) Executive Summary

Vision. A rule‑based, provable communication protocol for local‑first apps with near‑infinite aggregate throughput and ~0 marginal cost per update.

We enable ephemeral, high‑throughput sessions (games, collaboration, micro‑markets) that:

  • run locally with instant UX,
  • synchronize via event‑sourced channels,
  • derive state deterministically in an embedded SQL engine, and
  • resolve disputes on Ethereum/L2 with interactive one‑step proofs (no expensive per‑step ZK).

Reducers are WASM modules (e.g., AssemblyScript, Rust, Zig → WASM). Proof backend: a single WASM one‑step proof (WASM‑OSP).

Success metrics.

  • Dev adoption: 10+ independent apps using the framework launched in 6 months; 50+ in 12 months.
  • UX: <100ms perceived latency; zero gas per in‑session action.
  • Traction: 10k+ MAU on a showcase title.
  • Protocol quality: <1% disputes; zero loss of funds.
  • Perf: 1000+ updates/sec/channel locally; one‑step proof fits L2 gas budgets.

Practical note: “Near‑infinite” refers to off‑chain processing across many peers; bounded by device/network and rare on‑chain disputes.


2) Problem Statement

  • State channels have excellent UX for live, high‑frequency interactions (e.g., matches, collaborative editing).
  • They’re a poor fit for DeFi’s global liquidity and composability needs (which benefit from on‑chain shared state).

Historically, state‑channel stacks have lacked:

  • A simple application model for rich, non‑balance logic.
  • Queryable, verifiable state beyond balances.
  • A credible proof path without expensive per‑step ZK.

Thesis. The application layer should run at application latency with ~0 marginal cost per action, while remaining provable and settleable on‑chain when needed.

Approach. Combine an append‑only event log, a deterministic WASM reducer, an embedded SQL materialization, and a WASM one‑step proof for disputes.


3) Solution Overview

Three‑layer design

App (WASM reducer)
   ↓ messages
State Channel Engine — Event Store • Derivation (WASM) • Embedded SQL (PGlite)
   ↓ disputes/settlement
Ethereum/L2 — Adjudicator + WASM One‑Step Proof contracts
  • Local‑first: instant local application, durable sync from logs.
  • Provability: bisection to a single VM step; on‑chain one‑step verifier checks that step.
  • Determinism: no clocks, no non‑seeded RNG, no host I/O; bounded resources.

4) Core Concepts

  • Event‑sourced channels: source of truth is the message log.
  • Deterministic reducer (WASM): reduce(db_state_bytes, message_bytes) -> db_state_bytes.
  • Embedded SQL (PGlite): reducer drives a Postgres‑compatible subset compiled to WASM; storage is Merkleized to produce db_root.
  • Commitment: each signed state binds log + db_root.
  • Disputes: bisection over reducer steps → one‑step verification (WASM‑OSP).

5) System Architecture

Components

  • Reducer (WASM): app logic compiled to WASM.
  • Derivation Engine (Zig): loads reducer, executes steps, updates PGlite, emits snapshots.
  • Event Store: append‑only log (+ snapshots (db_state, db_root, turn)).
  • P2P Transport: libp2p or TCP/WebRTC fallback (pluggable).
  • Chain Service: handles adjudication & one‑step proofs.
  • On‑chain: Adjudicator + WASM one‑step verifier.

Proof‑friendly execution

State tuple S = { pc, regs/globals, mem_root, table_root, db_root }
(S, op) -> S'    // With Merkle witnesses for memory + DB leaves

6) Detailed Requirements

R6.1 Reducers

  • Load WASM reducers with caps (fuel/time/mem).
  • No host calls; deterministic imports only.
  • Stable reducer ABI and versioning.

R6.2 PGlite

  • Deterministic WASM build; host‑call isolation.
  • Merkleized storage; update db_root per step.
  • Stable snapshot format (serialize/deserialize) with versioning.

R6.3 Proof

  • WASM profile: no FP, bounded linear memory, no host I/O.
  • Bisection: canonical expansion from turn→steps; logarithmic depth.
  • One‑step API (Solidity): oneStepVerify(preState, op, witness, postState).

R6.4 Contracts

  • Adjudicator: deposits, challenges, withdrawals.
  • Proof Manager: bisection + WASM one‑step.
  • App Contract: validates append‑only appData, turn‑taking, reducer versioning.

R6.5 Messages

  • JSON initially, MessagePack optional.
  • Per‑message step budget.

7) Protocol Specifications

Versioned reducers

  • Channel fixedPart includes reducerId = H(code) || profile(WASM); appData commits to it. Upgrades → new channel.

Commitments

turn: i
appData: [m_1..m_i]
db_root: H(DB_i)
state_hash = H(channelId, appData_hash, outcome, turn, isFinal, db_root)

Both parties sign state_hash.

Disputes

  • Challenger submits (pre, post, range); defender splits; recurse to a single step.
  • Final step checked by oneStepVerify (WASM‑OSP).

8) Worked Examples

A) Tic‑Tac‑Toe Reducer (AssemblyScript → WASM)

Minimal example showing a deterministic reducer. Here the “DB” is a fixed‑size binary state for clarity (9 cells + next + winner). In production, reducers drive PGlite; the idea is the same: apply message → update state deterministically.

// assembly/index.ts (AssemblyScript)
// ABI: reduce(db_ptr, db_len, msg_ptr, msg_len, out_ptr_ptr, out_len_ptr) -> i32
// Return 0 on success, non-zero error code on invalid moves, etc.

const STATE_LEN: i32 = 11; // 9 cells + next + winner

// Cell values: 0 empty, 1 = X, 2 = O
// next: whose turn next (1 or 2)
// winner: 0 none, 1 X, 2 O, 3 draw

@inline
function loadU8(p: usize, off: i32): u8 { return load<u8>(p + <usize>off); }
@inline
function storeU8(p: usize, off: i32, v: u8): void { store<u8>(p + <usize>off, v); }

function checkWin(boardPtr: usize): u8 {
  // Winning triplets (indexes 0..8)
  const lines: StaticArray<i32> = [
    0,1,2, 3,4,5, 6,7,8, // rows
    0,3,6, 1,4,7, 2,5,8, // cols
    0,4,8, 2,4,6          // diags
  ];
  for (let i = 0; i < 8; i++) {
    let a = loadU8(boardPtr, unchecked(lines[i*3]));
    let b = loadU8(boardPtr, unchecked(lines[i*3+1]));
    let c = loadU8(boardPtr, unchecked(lines[i*3+2]));
    if (a != 0 && a == b && b == c) return a; // winner 1 or 2
  }
  // check draw
  for (let j = 0; j < 9; j++) if (loadU8(boardPtr, j) == 0) return 0;
  return 3; // draw
}

export function reduce(
  db_ptr: usize, db_len: i32,
  msg_ptr: usize, msg_len: i32,
  out_ptr_ptr: usize, out_len_ptr: usize
): i32 {
  // State layout (11 bytes): [0..8]=board cells, [9]=next, [10]=winner
  if (db_len != STATE_LEN || msg_len < 2) return 1;

  // Read state
  const boardPtr = db_ptr;
  let next = loadU8(db_ptr, 9);
  let winner = loadU8(db_ptr, 10);

  // Message layout: [0]=op, [1]=pos (0..8)
  // op: 1=move
  const op = loadU8(msg_ptr, 0);
  const pos = loadU8(msg_ptr, 1);
  if (op != 1 || pos > 8) return 2;

  // If game finished, reject
  if (winner != 0) return 3;

  // Apply move by 'next'
  let cur = loadU8(boardPtr, pos);
  if (cur != 0) return 4; // occupied
  if (next != 1 && next != 2) return 5; // invalid next

  storeU8(boardPtr, pos, next);

  // Check winner/draw
  winner = checkWin(boardPtr);
  storeU8(db_ptr, 10, winner);

  // Toggle turn if no winner/draw
  if (winner == 0) {
    next = <u8>(3 - next); // 1<->2
    storeU8(db_ptr, 9, next);
  }

  // Allocate output and copy updated state
  const out = memory.data(STATE_LEN);
  for (let i = 0; i < STATE_LEN; i++) {
    storeU8(out, i, loadU8(db_ptr, i));
  }

  // Write out pointer/len
  store<usize>(out_ptr_ptr, out);
  store<i32>(out_len_ptr, STATE_LEN);
  return 0;
}

Message encoding (example):

  • op=1, pos∈[0..8]. State encoding (11 bytes):
  • bytes [0..8] board cells, [9] next, [10] winner.

This reducer is pure, deterministic, bounded, and performs no host I/O.


B) Off‑Chain Initial Mint → On‑Chain Commit

Goal: compute an initial token distribution off‑chain (gasless UX), then commit the result on‑chain so users can claim their balances.

Flow

  1. Channel setup: issuer + auditor (or a set of signers) open a channel with a MintReducer.
  2. Off‑chain events: messages like Mint(to, amount) accumulate; the reducer builds a balances map and a running Merkleized DB state (db_root).
  3. Finalize: parties co‑sign the final db_root.
  4. On‑chain commit: submit db_root to an InitialMint contract gated by the adjudicator (optionally behind the dispute window).
  5. User claims: anyone can later claim(address, amount, proof) verified against the committed root.

Mint reducer invariants (examples)

  • Total supply ≤ cap.
  • No negative balances; Mint only allowed before Finalize.
  • Deterministic ordering; exact integer arithmetic.

Minimal Solidity sketch (read‑only verifier)

// Pseudocode: assumes adjudicator exposes a finalized (channelId, dbRoot).
interface IAdjudicator {
  function finalizedDbRoot(bytes32 channelId) external view returns (bytes32);
}

contract InitialMint {
  IAdjudicator public adjudicator;
  mapping(address => bool) public claimed;

  constructor(address _adj) { adjudicator = IAdjudicator(_adj); }

  // Merkle leaf = keccak256(abi.encode(addr, amount))
  function claim(bytes32 channelId, address to, uint256 amount, bytes32[] calldata proof) external {
    require(!claimed[to], "already claimed");
    bytes32 root = adjudicator.finalizedDbRoot(channelId);
    require(root != bytes32(0), "root not finalized");
    bytes32 leaf = keccak256(abi.encode(to, amount));
    require(verify(proof, root, leaf), "bad proof");
    claimed[to] = true;
    _mint(to, amount); // hook to a token or mint here
  }

  function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
    bytes32 h = leaf;
    for (uint i = 0; i < proof.length; i++) {
      bytes32 p = proof[i];
      h = (h < p) ? keccak256(abi.encode(h, p)) : keccak256(abi.encode(p, h));
    }
    return h == root;
  }

  function _mint(address to, uint256 amount) internal {
    // Implement: mint in this contract or call an external token with MINTER_ROLE.
  }
}

Why this fits the protocol

  • Off‑chain work is free and instant for users.
  • If someone cheats, counterparties challenge via WASM one‑step until the correct db_root is finalized.
  • On‑chain state only stores the succinct commitment (db_root); individual user mints are self‑serve with Merkle proofs.

9) Implementation Roadmap

Phase 1 — Core Engine & PGlite Spike (M1‑M3)

  • Event loop & objectives, Nitro‑style adjudicator, TCP P2P.
  • Compile pglite‑wasm; prove determinism; snapshots.
  • WASM reducer loader + resource caps; Tic‑Tac‑Toe demo.

Exit: 1000 turns/sec locally; cooperative close.

Phase 2 — WASM Proof Path (M4‑M6)

  • Restricted WASM profile + state tuple; Solidity one‑step verifier.
  • Merkle memory & DB witnesses; end‑to‑end dispute on a counter reducer.

Exit: Bisection + one‑step working on test reducer.

Phase 3 — Examples & SDK (M6‑M8)

  • Polished Tic‑Tac‑Toe example & off‑chain mint example.
  • SDK helpers (encodings, snapshots, test harness).

Phase 4 — Optional: Solidity Reducers via EVM‑in‑WASM (M8‑M10)

  • Guillotine integration (optional milestone), storage→DB adapter.

Phase 5 — Production Hardening (M10‑M12)

  • Audits, perf passes, ops playbooks; hosted hub economics (if any).

10) Technical Decisions & Rationale

  • Single backend (WASM‑OSP): aligns with browser/WASM dev flow; avoids a second toolchain; gas costs are explicit and tunable.
  • SQL materialization (PGlite): rich queries and audits; deterministic storage → db_root.
  • Event sourcing: source of truth is the log; re‑derive to audit; snapshots for speed.

11) Security Considerations

  • Proof soundness: formal WASM profile; adversarial test corpus; negative tests for forged witnesses and mismatched roots.
  • Sandboxing: hard caps (fuel/time/mem); no host calls.
  • Code‑hash pinning: reducerId in channel fixedPart.
  • Replay safety: signatures bind log + db_root.
  • Mint example: finalize root only after dispute window; claims are one‑way (no double claim).

12) Performance Targets

  • Local reducer step: <10ms p95; WAN perceived <100ms per action.
  • Cold re‑derive (1k msgs): <5s on laptop‑class hardware.
  • Bisection depth: O(log steps); one‑step gas: <1–2M on L2.
  • Commit sizes: db_root 32B; witnesses a few KB/step.

13) Out of Scope

  • Sync / custom consensus rules: we sync via custom rule‑based consensus, but the exact protocol is out of scope for this doc.
  • Adapters for Replicache/ElectricSQL: not supported here; they are prior art/inspiration, not part of the design.

14) Open Questions

  1. Exact restricted WASM opcode set (no FP/SIMD initially).
  2. PGlite Merkle layout: page vs. row → witness size vs. update cost.
  3. SQL subset guardrails (e.g., CTE recursion off by default).
  4. Reducer upgrades & migrations between reducerIds.
  5. Spectator feeds: db deltas vs. raw messages.
  6. Optional future: recursive proofs to batch steps (Mina‑style receipts).

15) References & Prior Art

  • State channels & force‑move: Nitro/go‑nitro, Perun, Counterfactual, Raiden.
  • Interactive fault proofs: Optimism Cannon (MIPS), Arbitrum Nitro (AVM one‑step), Truebit (historical).
  • zk & recursion (contrast / future compression): Mina Protocol (recursive zkSNARKs, o1js), RISC Zero, SP1, zkWASM, zkEVM, StarkNet/Cairo.
  • Deterministic WASM / EVM‑in‑WASM: deterministic WASM profiles; evmone‑wasm / Sputnik‑wasm; Guillotine (for later).
  • Event sourcing & embedded SQL: CQRS/event‑sourcing; pglite‑wasm.
  • Local‑first & sync systems (inspiration only): ElectricSQL, Replicache, CRDT literature.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment