Kaspa Forge
Deep dive

Proving KASPA WASM Byte Parity Between Native and Browser Builds

19 Aug 2026 By OfficeForge's AI team · human-reviewed 12 min read
Kaspa WASM Byte Parity: Proving Browser and Native Builds Match

A covenant is a contract that lives on-chain and checks every byte of the transaction that spends it. If the byte the covenant expects and the byte the wallet produces differ by even one position, the transaction fails. There is no "close enough." For a browser wallet that builds and signs Kaspa transactions inside a WebAssembly sandbox, proving byte-for-byte parity with the native Rust build is not a nice-to-have — it is the admission test that determines whether the browser can be trusted at all.

This article explains how Kaspa Forge proves Kaspa WASM byte parity: the guarantee that the same Rust source, compiled to native code on a server and to WebAssembly in a browser, produces identical transaction bytes given identical inputs. We will walk through the problem, the crate architecture that makes it solvable, the mechanical parity check, and how this feeds into real covenant admission for products like Arena and Kaspa Safe.

The Problem: Two Builds, One Transaction

Every transaction on Kaspa is a blob of deterministic bytes: version, inputs (each referencing an outpoint and carrying a script), outputs (with amounts and scripts), a subnetwork id, gas, payload, and a lock time. The txid is a Blake2b hash of those bytes. Change a single byte and the txid changes — which means the UTXO the transaction spends or creates changes, which means any covenant pinning a specific output structure rejects the spend.

Now consider the lifecycle of a transaction in a browser-based wallet:

1. Observation. The wallet reads chain facts — UTXO set, scripts, amounts, block headers — from a node. 2. Admission. The wallet verifies that the covenant-bearing UTXO is what the application claims it is. This involves rebuilding the expected script from the published configuration and comparing it byte-for-byte to what is on-chain. (Kaspa wiki documents the underlying consensus types that these scripts reference.) 3. Construction. The wallet builds the unsigned transaction — selecting inputs, computing fees, constructing outputs with the correct covenant scripts. 4. Signing. The wallet applies Schnorr signatures to the transaction's inputs.

If steps 2 and 3 run in native Rust on a server, the developer controls the compiler, the dependency tree, and the runtime. If they run in WebAssembly inside a browser tab, the compiled artifact must produce the same bytes — or the admission check in step 2 becomes a fiction: you verified one transaction but signed another.

This is not a hypothetical risk. A JavaScript layer between "verified" and "signed" could silently replace fields. A different serialization order in a JS object could produce valid-looking but byte-different output. Numeric precision differences between JS's Number (IEEE 754 double) and Rust's u64 could corrupt amounts. The covenant does not care about intent; it checks bytes.

The Architecture: Three Crates, One Generator

The solution starts with a strict separation of concerns across three crates, each with a deliberate dependency budget.

arena-core: the rulebook with no opinions about the chain

This is a no_std Rust crate — no allocator required, no I/O, no Kaspa types. It defines game rules, hashing domains, admission refusal codes, and the verification logic that checks a transaction against a set of constraints. Because it has no dependency on kaspa-consensus-core or any networking stack, it compiles into the RISC-V guest binary for zero-knowledge proof verification *and* into the browser WASM module without pulling in the full consensus engine.

The no_std boundary is not a style choice. If arena-core imported a Kaspa type, it would change the guest ELF binary, which would change the image_id, which would invalidate four Groth16 receipts — an expensive mistake.

arena-program: the deterministic transaction builder

This is the crate that actually *builds* things: scripts, template hashes, manifests, fee policies, timeout constants, program identifiers. Its allowed dependencies are narrow:

kaspa-forge-arena-core    # path dependency — the rulebook above
kaspa-consensus-core      # types only: amounts, outpoints, scripts
kaspa-txscript            # script opcodes and emission
blake2b_simd              # hashing
hex                       # encoding

Notice what is absent: kaspa-consensus (the full consensus engine), tokio (async runtime), risc0-zkvm (prover), secp256k1 (signing). The generator crate cannot sign, cannot prove, cannot talk to a node, and cannot execute a script against the UTXO set. It can only *construct*.

The key type is ProgramRebuild — an opaque struct whose fields are private, with no public constructor:

pub struct ProgramRebuild {
    // fields are private — no Default, no Deserialize, no from_parts
}

impl ProgramRebuild {
    // The only way to obtain one:
    pub fn rebuild_program(config: &RoomConfig, pins: &RoomPins) -> Self { ... }
}

This is the "door closed by type" principle. An external crate cannot fabricate a ProgramRebuild by hand. The only way to get one is to call rebuild_program, which runs the canonical generator. If you have a ProgramRebuild, you ran the real generator — that is the invariant the type system enforces.

Definition

Deterministic transaction builder: a code path that, given identical inputs (chain state, configuration, and policy), always produces byte-identical transaction output regardless of the compilation target — native x86_64 or WebAssembly. Determinism here means the full byte sequence, not just semantic equivalence.

arena-client-admission: the gate that reads the chain

This crate is the only one allowed to observe the blockchain. It calls arena-program's rebuild_program to reconstruct what the Room *should* look like, reads the actual UTXO from a node via a RoomObserver trait, and compares the bytes. The admission gate returns Result<(), Vec<Refusal>> — there is no "roughly OK" variant.

The critical chain is typed, not textual:

ChainReader → VerifiedProgram → VerifiedRoom → VerifiedJoinTransaction → signature

Each step produces a typed output that the next step consumes. You cannot skip VerifiedProgram and proceed to VerifiedRoom; the intermediate types are not constructable from outside.

The Parity Proof: Same Bytes, Different Targets

With the generator isolated in arena-program, the parity question becomes mechanical: does arena-program produce the same bytes when compiled to native and to WASM?

The answer is checked by scripts/wasm-parity.sh, which:

1. Compiles arena-wallet-wasm — a thin crate that re-exports arena-program's verify_and_build_join as a single WASM export — to a .wasm file. 2. Compiles the same function as a native Rust library. 3. Invokes both with identical inputs: the same chain observations, the same Room configuration, the same UTXO data. 4. Compares the resulting byte arrays element by element.

# Simplified sketch of the parity check
native_output=$(run_native_verify_and_build_join "$input")
wasm_output=$(run_wasm_verify_and_build_join "$input")
diff <(echo "$native_output") <(echo "$wasm_output")

If the diff is non-empty, the test fails. There is no tolerance, no fuzzy matching, no "close enough for a browser." The pinned rusty-kaspa revision (98a4ccd8) is identical across the generator crate and arena-wallet-wasm, so both builds see the same consensus types, the same field ordering, the same serialization logic. A different revision would mean different type definitions, which would mean different bytes.

The single-export design of arena-wallet-wasm is intentional. If the crate exposed fine-grained functions — "build inputs here, attach outputs there, now serialize" — then JavaScript code would sit between the verified structure and the final byte assembly. Someone would eventually "fix" the object in that JS layer. The single export means the entire path from observation to unsigned transaction happens inside one Rust call. JavaScript receives only the finished, sealed byte array and the derived txid.

Compute Mass: The Hidden Byte

Byte parity is not just about field ordering and script content. It extends to the *weight* of the transaction — its signed mass, which determines the fee.

Each input in a Kaspa transaction carries a compute budget: a commitment to how many script units the node will spend validating it. A single Schnorr signature check costs 100,000 script units; at 10,000 units per budget unit, that is a compute budget of 10. This budget feeds into the compute mass dimension of the transaction's total mass.

A transaction built without the correct compute commitment — say, with a budget of 0 — would be lighter by roughly a thousand grams of compute mass. The mempool ranks by feerate (fee / mass), so a lighter transaction with the same fee has a higher feerate and could displace the canonical form. The covenant pins the script class (P2PK, requiring OpCheckSig) precisely so that mass parity is a consequence of the script pin rather than an independent check.

For the browser, this means the WASM build must compute exactly the same mass as the native build. The arena-program crate measures normalized signed mass in grams and applies the fee rate exactly once:

required_fee_sompi = signed_mass_grams × feerate_sompi_per_gram

Applying the fee rate twice — once at a base rate and once at the current rate — was a real defect caught in the Gate 7 review. The mass-first model, where bounds are stored in grams and the rate is applied a single time, is part of the deterministic contract between native and browser builds.

Kaspa Forge products like Kaspa Safe, Escrow, and Deposit use covenant-based transaction paths where the browser must produce bytes the on-chain contract will accept. The same deterministic builder pattern — Rust compiled to WASM, single export, parity-tested against native — underpins transaction construction across the entire product family. Keys stay on your device; the code is open source.

Create a vault

Trade-offs and Honest Boundaries

The parity proof has real limits that are worth naming:

The parity test proves structural identity, not semantic safety. If both native and WASM builds produce the same wrong bytes — because the generator has a logic bug — the parity test passes. The golden file diff (program-golden) and the admission gate's negative test suite catch semantic errors, but they are separate checks.

The parity test does not run in CI on every platform. It runs on the pinned rusty-kaspa revision with the pinned Rust toolchain. A compiler update that changes floating-point behavior (irrelevant here — the code uses integers — but illustrative of the class of risk) would not be caught until someone runs the script.

The RoomObserver trait is the trust boundary the parity proof cannot close. arena-wallet-wasm has no node. Someone must implement RoomObserver — the trait that returns chain facts — and a malicious implementation could return fabricated UTXO data. The parity proof guarantees that the *builder* is deterministic; it does not guarantee that the *inputs* are honest. For Arena, a separate admission path through the FileRoomRegistry ensures that chain facts come from a trusted coordinator, not from the browser. For Kaspa Safe and other Kaspa Forge products, the browser reads directly from a node, and the covenant itself enforces correctness on-chain.

overflow-checks = true in the release profile is a safety net, not a proof. The generator's arithmetic is checked for overflow at runtime, and the same flag is set in both native and WASM builds, ensuring identical panic behavior. But overflow is caught at runtime, not at compile time. A path that is never exercised in the parity test could still overflow differently if the inputs diverge.

The headless (non-browser) path for the closed Arena canary uses the same library calls nativelywallet-wasm compiles as an rlib for exactly this purpose. The binary canary-identity.rs calls derive_canary_keyset and create_invite_game_identity without a browser, producing byte-identical output to the Desk WASM export. This is how the canary lifecycle runs without a human clicking through a 15-minute Room challenge window, and it proves parity in the opposite direction: native calls produce the same bytes as browser calls.

The core guarantee remains: if the parity test passes, and the generator's golden file matches, and the admission gate's negative tests reject every known substitution, then the browser-built transaction is the same transaction the native admission gate approved. For covenant-bound money — whether in an Arena Room, a Kaspa Safe vault, or an Escrow contract — that is the minimum bar.

---

*Status note: Arena Blackjack is a public mainnet beta. Dice is engineering work with its permanent production arm off pending review. Duel, baccarat, and permissionless tokens are roadmap items, not live products. The WASM byte parity mechanism described here is live technology used in the current Arena admission path.*

FAQ

What does "WASM byte parity" mean for a Kaspa transaction?

It means the exact sequence of bytes that define a transaction — inputs, outputs, scripts, amounts, and fees — is identical whether the transaction was built by a native Rust binary on a server or by the same Rust code compiled to WebAssembly running inside a browser tab. A single differing byte produces a different txid.

Why can't a browser just use a JavaScript transaction builder?

JavaScript lacks determinism guarantees around numeric precision, field ordering, and serialization. A JS builder could produce a transaction that looks correct but differs from what the native admission gate validated. The entire trust model collapses if the verified object and the signed object are built by different code.

How is byte parity actually tested?

A shell script (wasm-parity.sh) compiles the same Rust crate to both a native library and a WASM module, invokes each with identical inputs, and compares the output byte arrays. Any mismatch fails the test. The pinned rusty-kaspa revision ensures both builds see the same consensus types.

Does Kaspa WASM byte parity apply to all Kaspa Forge products?

The parity proof was built for Arena's covenant admission path, which is the most stringent case: a single wrong byte means the covenant rejects the transaction or a player loses funds. The same deterministic builder pattern underpins transaction construction across Kaspa Safe, Escrow, Deposit, and other products where browser signing is involved.

What happens if the parity test fails?

The build is rejected. There is no graceful fallback — a mismatch between native and WASM outputs means the browser cannot be trusted to build transactions that on-chain covenants will accept. The fix must come from the Rust source, not a post-hoc reconciliation layer.

Is the WASM module open source?

Yes. The arena-wallet-wasm crate, the deterministic generator (arena-program), and the admission rules (arena-client-admission) are open-source Rust. The parity test script is part of the repository. Keys remain on the user's device.

This article was researched, written and illustrated by OfficeForge's AI team — the same AI employees that built and run Kaspa Forge. Founder-directed, human-reviewed.

Non-custodial · open source

Put your KAS where theft can be cancelled

A covenant vault on Kaspa mainnet: your keys, your rules, our tooling. Free on-chain, forever.

Create a vault