Skip to content

Instantly share code, notes, and snippets.

@cmdruid
Created August 12, 2026 21:07
Show Gist options
  • Select an option

  • Save cmdruid/d53de14c3addc1e74be6c56c55f9e58a to your computer and use it in GitHub Desktop.

Select an option

Save cmdruid/d53de14c3addc1e74be6c56c55f9e58a to your computer and use it in GitHub Desktop.
Vault Wallet — Design Specification

Vault Wallet — Design Specification

Version 0.1.4 · Prototype (Chrome Extension)
Date: 2026-08-12
Status: Draft — OP_RETURN payload encoding finalized


1. Introduction

Vault Wallet is a Bitcoin custody protocol that implements a vault using pre-signed transactions and an ephemeral key covenant. The design protects funds against theft of the owner’s primary private key by enforcing a mandatory delay period after any attempt to open the vault, during which the legitimate owner can recover the funds using independent recovery paths (threshold multi-signature quorums optionally gated by hash-locks).

This approach requires no consensus changes (no soft-fork covenants such as OP_CTV or OP_VAULT). It relies on the secure generation and deletion of an ephemeral private key, together with encoding of vault parameters into an OP_RETURN output so that the “vault open” transaction can be deterministically reconstructed.

The initial prototype is a Chrome browser extension. A future production implementation may be rewritten in Rust for higher assurance and native performance.


2. Goals and Non-Goals

2.1 Goals

  • Prevent unauthorized (theft) transfers of vaulted Bitcoin even if the owner’s primary private key is compromised.
  • Provide a clear recovery window (the vault timer) during which the legitimate owner can claim funds using independent keys.
  • Support flexible recovery paths: threshold multi-sig (q-of-n) optionally preceded by one or more hash-locks.
  • Encode all necessary vault parameters on-chain (OP_RETURN) so the open transaction can be reconstructed without offline storage of the pre-signed transaction itself.
  • Deliver a usable Chrome-extension prototype that can create, open, and withdraw from vaults on Bitcoin testnet (and later mainnet).

2.2 Non-Goals (for v0.1)

  • Support for consensus-level covenants (CTV, OP_VAULT, APO, etc.).
  • Fully automated watchtower recovery (owner or a user-chosen service must still act).
  • Cross-input signature aggregation or advanced MuSig constructions beyond simple multi-sig scripts.
  • Hardware-wallet integration in the first prototype (software keys only).

Privacy note: The vault-owner public key is revealed on-chain when the vault is opened (script-path spend). This is accepted.


3. High-Level Architecture

The system consists of three logical components that will live inside the Chrome extension (and later the Rust implementation):

  1. Encoder / Transaction Builder — Constructs the Taproot trees, the Vault Deposit transaction (including OP_RETURN), and the pre-signed Vault Open transaction.
  2. Key & State Manager — Generates and securely discards the ephemeral keypair; stores the ephemeral public key + signature, recovery key material (or references to them), and vault metadata.
  3. Wallet Interface — UI for configuring vault parameters (timelock, quorums, hash-locks), signing with the vault-owner key, broadcasting, monitoring the timer, and constructing recovery spends.

All on-chain artefacts use Taproot (P2TR) outputs.

  • Deposit UTXO: Key-path spending is disabled using the BIP-341 NUMS point as internal key. Spending requires a script-path that is a 2-of-2 between the vault-owner public key and the ephemeral public key.
  • Open transaction: Constructed as a v3 (TRUC) transaction containing exactly one recovery output + one ephemeral anchor (P2A). The ephemeral signature covers the critical input/output pair; fees are supplied via CPFP on the anchor.
  • After Open: Funds sit in the recovery Taproot output whose script tree encodes the unilateral timelock path and the recovery quorum paths. The recovery output amount equals the vaulted amount exactly.

4. Lifecycle

The vault has three sequential stages:

  1. Vault Deposit — Funds are locked into a Taproot output (key-path disabled via NUMS). Vault parameters and the pre-signed open signature are written into an OP_RETURN output of the same transaction.
  2. Vault Open — The owner (or an attacker who has the vault-owner key) reconstructs the open transaction from the OP_RETURN data, supplies the vault-owner signature (script path), attaches the pre-stored ephemeral signature, and broadcasts the v3 transaction (possibly with a CPFP child paying the fee via the ephemeral anchor). Confirmation starts the relative timelock (CSV).
  3. Vault Withdraw — After the timer expires the vault owner can unilaterally spend with their key. At any time after open (including during the timer) any party that can satisfy a recovery path (all required hash pre-images + a quorum of signatures that includes at least one independent key) can spend the funds immediately.

5. Protocol Steps (Setup & Deposit)

  1. User configures the desired Taproot recovery tree: timelock value (relative CSV in blocks), one or more quorums, and optional hash-locks per quorum. If the user supplies a time duration, the encoder converts it to an approximate block count at creation time.
  2. User generates a fresh ephemeral keypair (secp256k1) at the moment of signing.
  3. Encoder builds the Vault Open transaction template:
    • Version 3 (TRUC).
    • Exactly one input: the future deposit outpoint.
    • Exactly two outputs: (1) the recovery Taproot output (amount = vault amount) containing the full script tree, (2) a zero-value ephemeral anchor (P2A).
    • The ephemeral private key signs with Schnorr + SIGHASH_ALL.
  4. The ephemeral private key is immediately zeroed (see §8.1) and discarded. Only the ephemeral public key and the pre-computed signature are retained. This deletion establishes the covenant.
  5. Encoder builds the Vault Deposit transaction:
    • One or more inputs funding the vault amount plus fees.
    • Primary output: P2TR with BIP-341 NUMS internal key (key-path disabled). Script path is a 2-of-2 of (vault-owner public key, ephemeral public key). Amount = vaulted sats.
    • Secondary output: OP_RETURN containing the serialized vault payload (see §7).
    • Optional change output.
  6. User signs the deposit transaction with their private key and broadcasts it.

6. Opening the Vault and Spending Paths

6.1 Opening

Anyone who knows the vault-owner private key can:

  1. Reconstruct the open transaction template from the deposit’s OP_RETURN data.
  2. Add the vault-owner signature on the script path.
  3. Attach the pre-stored ephemeral signature.
  4. (Optionally) create a CPFP child that spends the ephemeral anchor to pay the desired fee.
  5. Broadcast the package.

Because the open transaction is v3/TRUC + ephemeral anchor, fee bumping is possible without giving the opener arbitrary control over the recovery output. Once the open transaction confirms, the relative timelock on the recovery output begins.

While the timer is active the unilateral path is disabled. The only way to move funds is to satisfy one of the recovery spending paths.

6.2 Spending Paths (after open)

  • Unilateral path — After the relative CSV timelock (measured in blocks from the open confirmation) expires, a single signature from the vault-owner key can spend. No additional key is introduced for the unilateral path.
  • Recovery paths — Each path is one Taproot leaf containing zero or more hash-locks followed by a threshold multi-signature.

Hard rules (locked):

  • Every recovery path must contain at least one public key that is not the vault-owner key.
  • A pure hash-lock-only path is forbidden.
  • A pure 1-of-1 using only an independent key is allowed (though a 2-of-2 that also includes the vault-owner key is recommended for usability).
  • A quorum may include the vault-owner key, provided the threshold still requires at least one independent signature.
  • There is one leaf per recovery path. Hash-locks and the multi-sig are concatenated in a single script (no OP_IF branching inside the leaf).

Canonical leaf shape (hash-locks + multi-sig):

<hash1> OP_HASH160 OP_EQUALVERIFY   (or OP_SHA256 depending on type)
<hash2> OP_HASH160 OP_EQUALVERIFY
...
<pk1> OP_CHECKSIG
<pk2> OP_CHECKSIGADD
<pk3> OP_CHECKSIGADD
...
<q> OP_NUMEQUAL

(Modern Taproot multi-sig style using OP_CHECKSIG / OP_CHECKSIGADD.)

To satisfy a path the spender must:

  1. Reveal the pre-images for all hash-locks belonging to that path, and
  2. Provide a valid threshold of signatures from the public keys belonging to that quorum (respecting the independent-key rule above).

Important property of hash-locks vs signatures:

  • Hash-locks do not commit to the transaction contents. Once a pre-image is revealed on-chain it is public forever; an attacker who sees it can also use it.
  • Digital signatures must commit to the specific transaction (SIGHASH). A signature valid for one withdrawal is useless for another. Therefore a quorum must agree on the exact destination and amount.

7. OP_RETURN Data Payload Format

The OP_RETURN output of the deposit transaction carries a compact, versioned binary encoding of everything needed to reconstruct the open transaction and the subsequent recovery tree.

7.1 Design Principles

  • Little-endian integers.
  • Explicit type tags for public keys and hashes so the format can evolve without a full version bump.
  • Fixed-size fields where practical; length-prefixed vectors for variable data.
  • The payload must be sufficient, together with the deposit outpoint, to deterministically rebuild the open transaction template (v3, recovery script tree, ephemeral anchor) and to verify the pre-stored ephemeral signature.

7.2 Top-Level Layout

Offset  Size     Field
------  -------  ------------------------------------------
0       4        magic          = 0x56574C54 ("VWLT")
4       1        version        = 0x01
5       4        timelock       = u32 relative CSV (blocks)
9       1        ephemeral_pk_type
10      var      ephemeral_pubkey   (see Public Key encoding)
var     1        sig_len
var     var      ephemeral_signature  (Schnorr, 64 bytes typical)
var     1        num_quorums    = u8 (0–255)
var     ...      quorum[0]
var     ...      quorum[1]
...

7.3 Public Key Encoding

Every public key (ephemeral and recovery) is prefixed by a type byte:

Type byte Meaning Following bytes
0x02 Compressed secp256k1 (even y) 32-byte X coordinate
0x03 Compressed secp256k1 (odd y) 32-byte X coordinate
0x00 X-only (Taproot style) 32-byte X coordinate
0x01 Reserved

Total size for a compressed key: 1 + 32 = 33 bytes.
Total size for an x-only key: 1 + 32 = 33 bytes (type distinguishes it).

Recommendation for v0.1: always use type 0x02 / 0x03 (full compressed) for clarity and maximum compatibility. X-only can be added later if size becomes a concern.

7.4 Hash Encoding

Every hash-lock is prefixed by a type byte:

Type byte Meaning Following bytes
0x14 HASH160 20 bytes
0x20 SHA256 32 bytes
0x00–0x13, 0x15–0x1F, 0x21–0xFF Reserved

This makes the format self-describing and allows mixed hash types inside a single vault if desired.

7.5 Quorum Encoding

Each quorum is encoded as:

1 byte   quorum_id          (0–255, application-defined)
1 byte   threshold_q        (1 ≤ q ≤ n)
1 byte   num_hashlocks      (0–255)
1 byte   num_pubkeys        (1–255)   // must be ≥ 1 and at least one non-owner
num_hashlocks × (1 + hash_size)   typed hashes
num_pubkeys   × (1 + 32)          typed public keys

7.6 Complete Example (conceptual)

A vault with:

  • 144-block timelock
  • one quorum that is 2-of-3 with two HASH160 locks

would produce a payload whose structure is:

VWLT | 01 | 90000000 | 02 | <33-byte eph pk> | 40 | <64-byte schnorr sig>
| 01
| 00 | 02 | 02 | 03
| 14 <20-byte hash1> | 14 <20-byte hash2>
| 02 <33-byte pk1> | 03 <33-byte pk2> | 02 <33-byte pk3>

7.7 Reconstruction Rules

Given a deposit transaction:

  1. Locate the OP_RETURN output and parse the payload according to the layout above.
  2. Verify magic + version.
  3. Extract timelock, ephemeral public key, and ephemeral signature.
  4. Build the recovery Taproot script tree from the quorum list (one leaf per quorum, using the typed hashes and keys, modern CHECKSIG/CHECKSIGADD).
  5. Construct the open transaction template:
    • nVersion = 3
    • 1 input = the deposit outpoint (sequence final or as required by TRUC)
    • Output 0 = P2TR(recovery tree), value = exact vault amount
    • Output 1 = ephemeral anchor (P2A), value = 0
  6. Verify that the supplied ephemeral signature is valid for this template under SIGHASH_ALL + Schnorr.
  7. The opener adds the vault-owner script-path signature and (optionally) a CPFP child spending the anchor.

8. Security Model & Considerations

The core security property is that possession of the vault-owner private key alone is insufficient for an instantaneous theft. An attacker who obtains the key can only broadcast the open transaction, which starts the publicly visible timer. During that window the legitimate owner (or any party controlling a recovery path that satisfies the independent-key rule) can move the funds to safety.

Because the unilateral key is the vault-owner key, an attacker who opens the vault and then waits out the timer will eventually be able to claim the funds if no recovery path is executed. The recovery paths are therefore the sole defense during the window.

8.1 Critical Requirements

  • Ephemeral key lifetime is minimized. The key is generated only at the moment of signing the open template. Immediately after the signature is produced the private key material is overwritten with crypto.getRandomValues and the reference is set to null. Residual risk of memory scraping is accepted for the Chrome prototype; a future Rust implementation will use stronger guarantees.
  • Deposit key-path is provably disabled. The internal key is the BIP-341 NUMS point
    H = lift_x(0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0).
    No private key for this point is known or generatable.
  • Recovery keys must be independent. No recovery private key may be stored together with the vault-owner key, nor be derivable from it. They should live on separate devices or with independent parties.
  • Hash pre-images must also be protected. Once revealed they are public.
  • Monitoring is required. The owner (or a service they configure) must notice when an open transaction appears on-chain and act within the timer.
  • Key reuse across vaults is discouraged but permitted. It is useful for non-interactive enrollment of third-party recovery services. The risk of cross-vault compromise is accepted and documented.

8.2 Known Limitations (inherent to deleted-key covenants)

  • Amounts and the open-transaction structure (except fees) are committed at deposit time.
  • Secure deletion of the ephemeral key is operationally hard to prove in a browser environment.
  • If the owner loses all recovery material and the timer is started by an attacker, the funds become available to the attacker after the delay.
  • The design relies on modern mempool policy (TRUC / package relay / ephemeral dust). Older nodes may not relay the open package as efficiently.
  • No artificial minimum vault size is imposed beyond the protocol dust limits. Users may create small experimental vaults; they remain responsible for ensuring the amounts are large enough to be economically spendable after fees.

9. Prototype Implementation Plan (Chrome Extension)

Target environment: Manifest V3 Chrome extension, JavaScript/TypeScript, using well-audited libraries.

9.1 Recommended Libraries

  • @noble/secp256k1 and @noble/hashes for cryptography
  • bitcoinjs-lib (or a modern fork that supports Taproot, v3 transactions, and P2A) for transaction construction and address handling
  • bip32 / bip39 for key derivation if hierarchical keys are used for the vault-owner key
  • A light client or Electrum/Esplora endpoint for broadcasting and monitoring (testnet first). The explorer URL is user-configurable.

9.2 Core Modules

  1. vault-encoder.ts — Builds the recovery Taproot tree (one leaf per path, modern multi-sig opcodes), the open transaction template (v3 + ephemeral anchor), signs with ephemeral key (Schnorr + SIGHASH_ALL), serializes the OP_RETURN payload according to §7, and constructs the deposit transaction (NUMS internal key).
  2. key-manager.ts — Generates ephemeral keys at signing time, performs the documented zeroing sequence, stores the retained pubkey + signature and vault metadata in chrome.storage.local.
  3. vault-ui — Screens for: create vault, review & sign deposit, list open vaults, reconstruct & broadcast open, monitor timer, construct recovery spend.
  4. network.ts — Thin wrapper around a Bitcoin API. Supports package relay for the open + CPFP child.
  5. watcher.ts — Background service worker that monitors for open transactions and raises notifications.

9.3 Development Phases

  • Phase 1 — Core cryptography & encoder (unit-testable without UI): key generation, Taproot tree construction (NUMS + 2-of-2 deposit, recovery leaves), OP_RETURN serialization/deserialization per §7, pre-signing the open tx template (v3 + anchor, Schnorr + SIGHASH_ALL), deterministic reconstruction, zeroing.
  • Phase 2 — Deposit & Open flows on testnet with a simple UI (including CPFP fee bumping).
  • Phase 3 — Recovery path spending (hash-lock + multi-sig that obeys the independent-key rule) and unilateral after timer.
  • Phase 4 — Hardening (better storage, fee estimation, service-worker watcher, documentation).

10. Future Work

  • Rust re-implementation for stronger key-handling guarantees.
  • Hardware-wallet support.
  • Optional dedicated watchtower service.
  • Migration path to native covenant opcodes once available.
  • Support for partial unvaulting / change handling.

11. Locked Decisions

# Decision Status
1 Ephemeral private key is generated only at signing time and discarded immediately. Zeroing = crypto.getRandomValues overwrite of the key material + null the reference. Locked
2 Deposit key-path is disabled with the BIP-341 NUMS point H = lift_x(0x50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0). Locked
3 Open spend uses script path only. Vault-owner public key is revealed on open. Locked
4 Open transaction is v3 (TRUC) and contains exactly one recovery output + one ephemeral anchor (P2A). Fees are paid via CPFP on the anchor. Locked
5 Every recovery path must include at least one non-owner public key. Pure hash-lock paths are forbidden. Locked
6 A quorum may include the vault-owner key, but the threshold must still require ≥1 independent signature. Locked
7 Pure 1-of-1 using only an independent key is allowed (2-of-2 with owner recommended). Locked
8 Monitoring is implemented via a service worker that polls a user-configured explorer URL. Full automated watchtower is future work. Locked
9 Key reuse across vaults is discouraged but explicitly permitted. Locked
10 Unilateral key after the timer is the vault-owner key. No separate unilateral key is introduced. Locked
11 One Taproot leaf per recovery path. Hash-locks + multi-sig are concatenated in a single script (no OP_IF). Locked
12 Multi-sig uses modern Taproot style (OP_CHECKSIG / OP_CHECKSIGADD). Classic OP_CHECKMULTISIG is not used. Locked
13 Timelock is pure relative CSV measured in blocks. User-supplied time durations are converted to block counts at vault creation. Locked
14 No artificial minimum vault size beyond protocol dust limits. Users may create small experimental vaults. Locked
15 Ephemeral signature uses Schnorr + SIGHASH_ALL. Locked
16 Recovery output amount equals the vaulted amount exactly. Zero-value ephemeral anchor + TRUC/ephemeral-dust policy is relied upon for fees. Locked
17 OP_RETURN payload uses explicit type tags for both public keys and hashes (see §7). Locked

12. OP_RETURN Encoding Summary (v0.1)

  • Magic: VWLT (4 bytes)
  • Version: 0x01
  • Timelock: u32 blocks
  • Ephemeral public key: typed (1-byte type + 32-byte data)
  • Ephemeral signature: length-prefixed Schnorr (SIGHASH_ALL)
  • Quorums: each carries typed hashes + typed public keys
  • Hash types: 0x14 = HASH160, 0x20 = SHA256
  • Public-key types: 0x02/0x03 = compressed, 0x00 = x-only (reserved for later)

This format is self-describing, versioned, and sufficient for deterministic reconstruction of the open transaction.


This document is a living specification. Decisions from design grilling are recorded in §11 and the body is updated accordingly.

End of Design Specification — Vault Wallet v0.1.4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment