When you deposit KAS into a Kaspa Safe vault, your browser constructs a transaction that satisfies a specific on-chain script — a covenant. That script enforces a withdrawal delay, lets an alarm key cancel a theft in progress, and can auto-transfer funds to an heir. The server, meanwhile, monitors the vault's UTXOs, sends Telegram and email alerts, and broadcasts the keyless complete transaction once the delay expires.
Both sides must agree on every detail of the covenant: the opcode order, the parameter encoding, the fee-budget ceiling. In a custodial service, only the server needs to know the rules. In a non-custodial one — where the browser holds the keys and the server never sees them — both compute independently and must reach the same answer.
WebAssembly (WASM) — a binary instruction format that runs in every major browser at near-native speed. Languages like Rust compile to WASM once and the output executes identically on any platform — no interpreter, no runtime variability.
Kaspa Forge solves the browser–server sync problem by compiling a single Rust codebase into two targets: WebAssembly for the browser and a native binary for the server. One source of truth, two runtimes. This article explains how that works, why it matters for the security of your funds, and where the design has honest limits.
What the Core Contains
The shared library — a single Rust crate — contains every piece of logic that both the browser and server need to handle Kaspa's covenant-based contracts. It is organized into focused modules:
core.rsholds all vault logic: address derivation from public keys, transaction builders for each of the seven spending paths (initiate, cancel, complete, checkin, inherit-auto, inherit-signed, migrate), value-conservation checks, fee-budget enforcement, and a sweep function for cleaning stray UTXOs off a vault address.escrow_core.rsholds the ten escrow spending paths — release, refund, mutual, dispute, auto-release, three arbitrate variants, two timeouts — reusing the lower-level primitives fromcore.rs.derive.rsimplements deterministic key derivation: from a single master seed, it generates vault keys, escrow keys, and wallet keys using HMAC-SHA512 with domain-separated paths (kaspaforge/v1/vault/<index>, etc.).chat_cipher.rsandchat_payload.rshandle the end-to-end encryption protocol (ECIES with ChaCha20-Poly1305) used in escrow deal chats.age_crypto.rsencrypts and decrypts the Desk profile — the.agekey-file that backs up the entire HD seed under a passphrase.txjson.rsdefines the shared JSON schema for passing transaction data between browser and server.
Together these modules cover vault creation, escrow deal lifecycle, E2E messaging, key derivation, and profile backup — the full contract layer of every Kaspa Forge product.
One Crate, Two Targets
The key architectural choice is how the crate compiles. Rust's build system lets one crate produce multiple artifacts. The Cargo.toml declares:
[lib]
crate-type = ["cdylib", "rlib"]
The cdylib target compiles to WebAssembly via wasm-pack. The rlib target links into the native server binary. Functions that only make sense in the browser — JavaScript bindings for key generation, address formatting, transaction signing — are gated behind a compile-time check:
#[cfg(target_arch = "wasm32")]
pub fn gen_keys(seed: &[u8], index: u32) -> JsValue { /* ... */ }
The server binary never includes these browser-specific exports. The WASM module never carries server-only code. Each target gets exactly what it needs from the same source.
The contract source itself enters the crate through Rust's include_str! macro. The Silverscript files — the on-chain scripts that define vault and escrow rules — are read at compile time and embedded as string constants. When the WASM module constructs a transaction in your browser, it compiles these strings into opcodes. When the server validates a submitted transaction, it uses the same compiled constants. There is no second copy that could drift, no runtime fetch that could be intercepted.
The crate depends on the rusty-kaspa libraries (tag v2.0.1), which provide the cryptographic primitives and transaction types aligned with Kaspa's consensus — the same foundations described in the Kaspa wiki for developers building on the protocol. Because the dependency is pinned to a specific tag, both browser and server see identical type definitions for UTXOs, script public keys, and transaction fields.
Same Language, Two Sides
The browser builds and signs transactions locally. It then sends the signed bytes to the server for broadcasting to the Kaspa network. But the server also needs to understand what was built — for validation, for its watcher logic, and for constructing its own keyless transactions (like complete or auto-release).
The txjson module defines a shared JSON schema for this hand-off. Transaction amounts are represented in sompi (the smallest KAS unit, 1 KAS = 100,000,000 sompi). The schema carries enough information for the server to verify that a browser-submitted transaction matches the expected covenant path — without ever seeing a private key.
One practical detail worth noting: sompi values are 64-bit integers, which exceed JavaScript's safe integer range. The browser-side bindings handle this with string serialization; the server parses them back to native integers. It is the kind of mismatch that a shared Rust type system catches at compile time rather than at runtime on someone's vault.
Snapshot Generations
Kaspa Forge did not ship everything at once. Safe came first, then Escrow, then the unified Desk. Each product introduced new WASM functions — escrow needed its ten spending paths and chat encryption; Desk needed HD derivation and profile management.
Rather than rebuilding every page whenever any module changed, each product pins to an immutable WASM snapshot — a frozen build artifact in the web asset directory. Once deployed, its bytes never change.
| Snapshot | Loaded by | Key capabilities |
|---|---|---|
| v3 | Safe pages (app.js) | All 7 vault paths, sweep, legacy desk helpers |
| v5 | Escrow pages (escrow.js) | All 10 escrow paths, E2E chat, mutual signing |
| v7 | Legacy desk page | Frozen reference snapshot |
| v8 | React Desk (vault-core-v8) | Full Desk: HD derivation, profile encryption, wallet send, Safe + Escrow + Deposit |
A Safe page loads snapshot v3. An Escrow page loads snapshot v5. The production React Desk loads v8, which includes the complete vault and escrow logic alongside Desk-specific features. Each snapshot is served as an immutable file — once deployed, it cannot be changed without uploading a new file with a new path.
This design has a direct safety benefit: a Desk update cannot accidentally break the Safe page's transaction builders, because the Safe page still loads the same v3 bytes it loaded before the update. Products are isolated at the binary level, not just the module level.
How It Works in Production
When a user opens the Safe creation wizard, the browser loads the v3 WASM module. The user generates a hot key and an alarm key — both derived from the HD seed stored in their encrypted Desk profile. The module computes the vault address from these keys and the delay parameter. The server receives only the public keys and parameters — never the seed.
When the user funds the vault, the server's watcher detects the incoming UTXO by polling the Kaspa node. If the user initiates a withdrawal, the browser builds the initiate transaction using the v3 module, signs it with the hot key, and submits it through the server's API. The server validates the transaction structure — using the same core.rs logic compiled into its native binary — and broadcasts it to the network.
For keyless paths like complete (no signature required, only that the DAA-score delay has elapsed), the server builds and broadcasts the transaction itself. The browser and server agree on the exact conditions because they share the same code.
The Desk ties everything together. One encrypted profile — backed by a single .age key-file under a passphrase — holds the master seed from which all keys derive. Whether the user is interacting with a vault, an escrow deal, or sending KAS from their wallet, the same derivation logic runs in the same WASM module.
What This Means for Self-Custody
The non-custodial guarantee rests on a simple fact: private keys exist only in the browser. The server receives public keys, parameters, and pre-signed transactions. It can watch, alert, and broadcast — but it cannot forge a signature.
If the Kaspa Forge service goes offline, the vault remains an on-chain contract on the Kaspa blockchain. The same Rust core is published as open source, and the vaultctl command-line tool — compiled from the identical crate — can interact with any Kaspa v2+ node to complete a withdrawal, cancel an unvaulting, check in to reset the inheritance timer, or migrate funds to a new vault. The contract does not know or care whether kaspaforge.org exists.
Try it yourself. Create a Kaspa Safe vault and see the on-chain covenant in action — withdrawal delays, alarm key cancels, and optional inheritance, all enforced by the blockchain rather than by a server. On-chain operations are free; you only need a Kaspa address to fund the vault.
Trade-offs and Honest Limits
The snapshot-generation model works, but it is technical debt. Four active snapshots means four sets of glue code to maintain. A contract change — say, adding a new guard in a future v4 of vault.sil — requires updating the snapshots that contain vault logic (v3 and v8), rebuilding both, redeploying, and verifying that each page's JavaScript glue still references the correct function names. The team manages this with pinned build scripts and integration tests, but it is a coordination cost that a single universal snapshot would eliminate.
The include_str! approach pins contract source at compile time. This is a feature — no runtime fetch that could be tampered with — and a constraint — a contract upgrade requires a full WASM rebuild and redeploy, not just a server restart. For a system where the entire point is that the rules are immutable once deployed, this is arguably the right trade-off.
There is also the broader limitation that browser-side computation is only as trustworthy as the code delivery. The WASM module is served over HTTPS from the same origin as the page. Users who want maximum assurance can verify the published source against the deployed bytes, or run the open-source vaultctl directly against their own Kaspa node. The architecture makes this verification practical — one crate, published source, deterministic compilation — but it does not eliminate the trust step entirely. It reduces it to something concrete and auditable, which is the most a non-custodial web tool can honestly offer.
FAQ
Why compile to WebAssembly instead of just using JavaScript?
Rust compiles to deterministic machine code via WASM. The same function produces identical output whether it runs in your browser or on the server. JavaScript is flexible but loose — a type-coercion bug could silently build an invalid covenant transaction. The Rust compiler catches these errors at build time.
What is a snapshot generation and why are there several?
As Kaspa Forge shipped new products (Safe, then Escrow, then Desk), each needed new WASM functions. Rather than rebuilding every page on each core change, each product pins to an immutable snapshot — v3 for Safe pages, v5 for Escrow, v8 for the production Desk. A Desk update cannot break the Safe page's transaction builders.
Can the server sign transactions without the browser?
No. The server has the same Rust logic compiled in, but it never holds private keys. It can build keyless transactions (like complete after a vault delay expires) and validate browser-submitted ones, but signing is a browser-only operation.
What happens to my vault if Kaspa Forge shuts down?
The vault is an on-chain Kaspa script. The open-source vaultctl CLI tool, built from the same Rust core, can interact with any Kaspa v2+ node to complete withdrawals, cancel unvaultings, check in, or migrate funds. The contract enforces the rules regardless of whether kaspaforge.org exists.
How does the browser get the contract rules?
The Silverscript source for each contract is compiled into the WASM binary at build time via Rust's include_str! macro. The browser never fetches rules from the server — they are baked into the module it loads.
Is the code auditable?
Yes. The Silverscript contracts, the Rust core crate, and the vaultctl CLI are published on GitHub. Anyone can compile the crate, verify the output matches the deployed WASM, and confirm that the on-chain scripts match the 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
