Skip to content

Instantly share code, notes, and snippets.

@RobinLinus
Last active May 14, 2026 10:40
Show Gist options
  • Select an option

  • Save RobinLinus/caaf998ca559e66ea4366fb3a6f5be55 to your computer and use it in GitHub Desktop.

Select an option

Save RobinLinus/caaf998ca559e66ea4366fb3a6f5be55 to your computer and use it in GitHub Desktop.
A Self-Custodial 2FA Vault with Built-in Recovery and Inheritance

A Self-Custodial 2FA Wallet with Built-in Recovery and Inheritance

Overview

This protocol describes a self-custodial Bitcoin wallet secured by a 2FA cosigning server, with two additional spending paths: a user-controlled unilateral recovery path (in case the server disappears or refuses to sign) and an heir-controlled inheritance path. The design improves on two families of existing wallets:

  • Cosigner-based 2FA wallets like Blockstream Green's Multisig Shield use a CSV that begins ticking at deposit confirmation. As the timelock approaches expiry, the user must redeposit funds to a fresh address to keep 2FA protection alive.
  • Timelock-based recovery/inheritance wallets like Wizardsardine's Liana likewise anchor their timelocks to deposit confirmation. Liana users must periodically "refresh coins" before the backup path becomes prematurely spendable.

This protocol's timelock is anchored to recovery initiation, not deposit. Funds can sit indefinitely without losing primary-path protection — no redeposits, no coin refreshes.

Key properties:

  • No periodic refreshes. Deposit UTXOs are stable for as long as the user wants.
  • Built-in inheritance. A dedicated tapleaf gives a designated heir a delayed claim path, preemptible by the user any time before the delay expires.
  • Cryptographic custody is preserved in every single-key-loss case, including loss of the user's key (via the heir branch). Operational liveness during the recovery window is a separate concern — see Failure Cases.

The protocol relies on a new Tapscript opcode, OP_TEMPLATEHASH2, which commits to the other inputs of the spending transaction rather than its outputs. This opcode is the natural complement to OP_TEMPLATEHASH (BIP-446) and this scheme is a primary motivating use case for adding it. The same primitive also enables a refresh-free variant of Liana-style decaying multisig — see Extensions.


Roles

  • User. Owns the funds. Holds a single key (typically on a phone). Authenticates to the server with a separate 2FA factor (TOTP, hardware token, etc.) ideally on a different device.
  • Server. 2FA cosigner. Holds one key. Cosigns spends via the Taproot key path when the user's 2FA succeeds. Watches the chain for activity on user addresses and alerts the user. Optionally implemented in a TEE.
  • Heir. Holds a key authorizing the inheritance branch. Can be a person, a lawyer, a cold backup seed owned by the user, or any party trusted as a delayed-access fallback.

OP_TEMPLATEHASH2

OP_TEMPLATEHASH2 is a proposed Tapscript opcode mirroring OP_TEMPLATEHASH (BIP-446) but committing to the spending transaction's other inputs instead of its outputs.

The hash commits to:

  • nVersion of the spending transaction (must be ≥ 2 for BIP68 sequence-locks to apply).
  • nLockTime.
  • For one designated co-input (always the transaction's first input):
    • the prevout (txid:vout),
    • the nSequence, with the BIP68 disable bit required to be unset.
  • Annex presence, mirroring BIP341/BIP446 conventions.

It deliberately does not commit to:

  • The full set of inputs. Only the designated co-input is committed. Additional inputs are permitted — for fee-bumping, or for batched recovery across multiple deposit UTXOs each referencing its own co-input.
  • Outputs or amounts. Output choice and fee-rate selection are delegated to a SIGHASH_DEFAULT signature on the co-input.

Committing to nVersion is load-bearing: without it, a nVersion = 1 transaction could satisfy the co-input commitment while ignoring BIP68 entirely, bypassing the delay. Requiring the BIP68 disable bit to be unset closes the same gap from the other direction.

The opcode expresses: "this output may be spent only if a v2-or-later transaction also includes the designated UTXO as an input, at the specified nSequence." Other inputs and all outputs are unconstrained.


Deposit Address

The deposit address is a Taproot output with the MuSig2 aggregate of the user and server keys as the internal key, and a script tree with two leaves:

Key path:    MuSig2(user, server)              ← normal 2FA spend
Script path:
  Leaf 1 — recovery     (co-input: recoveryUTXO,    nSequence = 4w)
  Leaf 2 — inheritance  (co-input: inheritanceUTXO, nSequence = 6mo)

Normal payments use the key path and look identical on-chain to any single-key Taproot spend (BIP327 MuSig2 produces a normal BIP340 aggregate key). The script tree is revealed only during a recovery or inheritance event.

Both leaves have the same shape:

<template_committing_to_co_input_outpoint_and_nSequence>
OP_TEMPLATEHASH2
OP_EQUALVERIFY

No signature check on the leaf itself. Authorization is the 64-byte Schnorr signature on the co-input. Delays are user-tunable at deposit time (defaults: 4 weeks recovery, 6 months inheritance).


Kickoff Setup (Two-Phase)

A naive design where kickoffUTXO is a sibling output of the deposit transaction creates a hash cycle: the deposit txid depends on its script, the script commits to recoveryUTXO's outpoint, and that outpoint depends on kickoffTx, which spends an output of the same deposit transaction. Setup is therefore split into two phases.

Phase 1: Establish kickoff roots

Before any deposits, the user creates and confirms on-chain two kickoff roots:

  • kickoffRoot — anchors the recovery chain.
  • kickoffRoot2 — anchors the inheritance chain.

Each root is a Taproot output with two leaves:

Leaf A (canonical kickoff):
  OP_TEMPLATEHASH commitment to the canonical kickoffTx template
  + user signature
  (for kickoffRoot2: + user-OR-heir signature)

Leaf B (destroy):
  User signature only

Leaf A is what's used in normal operation: it restricts the root to producing exactly the canonical kickoff and nothing else. The signature requirement matters because covenant-only would let any third party broadcast kickoffTx without a key, enabling anonymous harassment.

Leaf B lets the user voluntarily destroy a root (e.g. to rotate the kickoff chain or to disinherit). Only the user holds this authority on either root; the heir cannot strand inheritance.

Phase 2: Precompute kickoff transactions and derive deposit addresses

Given the confirmed roots, the user precomputes the canonical kickoffTx and kickoffTx2 templates. Each produces:

  • One or more recoveryUTXO_i (or inheritanceUTXO_i) outputs — one per deposit address the user expects to use in this segment.
  • A next_kickoffRoot (and next_kickoffRoot2) of the same form as the original, to extend the chain when this segment is exhausted.
  • An ephemeral anchor output (Bitcoin Optech: ephemeral anchors) to allow fee-bumping at broadcast time without changing the kickoff's txid.

Deposit addresses are then derived by committing the appropriate recoveryUTXO_i and inheritanceUTXO_i outpoints into the leaf templates.

Fee-bumping

kickoffTx is fixed-txid: its hash is committed in kickoffRoot's covenant and in every deposit script that references its outputs. It cannot be RBF'd. Fee-bumping happens via CPFP on the ephemeral anchor at broadcast time.

Chain extension

When a segment's slots are exhausted, the user spends next_kickoffRoot via its Leaf A (covenant) to produce the next segment, repeating the structure. Extension can happen at any time and requires only the user's signature.


Recovery and Inheritance Token Scripts

recoveryUTXO and inheritanceUTXO are Taproot outputs with an unspendable NUMS internal key (no key path) and script-path leaves:

recoveryUTXO — single leaf:

OP_SIZE 64 OP_EQUALVERIFY
<user_pubkey>
OP_CHECKSIG

inheritanceUTXO — two leaves:

claim   (heir):  OP_SIZE 64 OP_EQUALVERIFY <heir_pubkey>  OP_CHECKSIG
cancel  (user):  OP_SIZE 64 OP_EQUALVERIFY <user_pubkey>  OP_CHECKSIG

The OP_SIZE 64 OP_EQUALVERIFY prefix forces a 64-byte Schnorr signature, which on Taproot implies SIGHASH_DEFAULT (full transaction commitment) per BIP341. Without this constraint, the security of the all-outputs commitment depends on the wallet always picking the right sighash — a footgun. With it, the script rejects any other choice at consensus level.


Spending Flow

Normal payment

  1. User constructs the payment spending the deposit UTXO via the Taproot key path.
  2. User produces a MuSig2 partial signature and submits it to the server with 2FA.
  3. Server verifies 2FA and active policy (see Server-Enforced Spending Policies); produces its partial signature.
  4. Aggregated signature spends the key path. On-chain indistinguishable from a single-key Taproot spend.

Change is sent to a fresh deposit address committing to the next unused slot pair in the current segment.

Unilateral recovery

  1. User broadcasts kickoffTx (via kickoffRoot's Leaf A), producing all recoveryUTXO_i and next_kickoffRoot.
  2. User waits for the recovery delay from kickoffTx's confirmation.
  3. User constructs a recovery transaction including:
    • The deposit UTXO(s) being recovered, spent via Leaf 1.
    • The corresponding recoveryUTXO_i for each, with nSequence = recovery delay. Multiple deposits sharing one segment can be recovered in a single transaction.
    • User's 64-byte signature on each recoveryUTXO_i input, using its leaf.
  4. User broadcasts.

Inheritance

Symmetric to recovery, using the inheritance chain, longer delay, and the heir's signature via the claim branch of inheritanceUTXO_i.


Cancellation

The server watches all deposit addresses and the kickoff roots; on confirmation of kickoffTx or kickoffTx2 it alerts the user.

To cancel an active recovery, the user broadcasts a single transaction spending every live recoveryUTXO_i in the active segment, plus next_kickoffRoot via its Leaf B (destroy). Spending only some recovery tokens leaves other deposits in the same segment recoverable after the delay. Spending next_kickoffRoot prevents the attacker from extending the chain.

After cancellation, the deposit script's recovery leaf is permanently unsatisfiable. The user MUST then re-deposit through the key path to a fresh script tree. The key path remains live throughout, so this is straightforward when the server is reachable.

Inheritance cancellation is symmetric, using the cancel branch of each inheritanceUTXO_i.

Cancelling an unconfirmed kickoff is a double-spend of kickoffRoot via Leaf B, which doesn't preserve the kickoff chain either — the canonical kickoffTx outpoints stop being reachable. A re-deposit is required regardless of whether cancellation happens before or after confirmation.


Heir Discovery via Deterministic Derivation

The heir doesn't need an enumerated list of the user's deposits. Given a minimal inheritance packet, the heir can deterministically derive every deposit address the user has ever used and find their UTXOs by scanning the chain.

The packet contains:

- user_xpub
- server_xpub
- heir_privkey                       (the heir's own; not derived from user)
- recovery_chain_root_outpoint       (txid:vout of kickoffRoot)
- inheritance_chain_root_outpoint    (txid:vout of kickoffRoot2)
- recovery_delay, inheritance_delay

It is small, static, and can be sealed at wallet setup. It doesn't need updating as the user makes new deposits.

Discovery:

  1. Walk the inheritance chain forward from kickoffRoot2. Each kickoffTx2_N produces some inheritanceUTXO_N,i and a next_kickoffRoot2_N+1.
  2. Walk the recovery chain in parallel. Each kickoffTx_N produces matching recoveryUTXO_N,i. Both chains share derivation indices (N, i).
  3. For each (N, i), derive the deposit script. User and server keys are derived from the two xpubs at deterministic paths. Combined with both co-input outpoints, the heir's pubkey, and the delays, the heir reconstructs the exact two-leaf Tapscript tree and computes the deposit address.
  4. Check for UTXOs. Any UTXO found at a derived address is claimable via Leaf 2 after the inheritance delay.

The same scheme works for the user themselves on the recovery path.


Server-Enforced Spending Policies

The server can enforce user-signed policy on the key path. This adds a layer of defense against coercion ($5-wrench attacks), compromised devices, and momentary lapses.

The user signs a policy commitment and registers it with the server. The policy can specify:

  • Daily / weekly / monthly spending caps.
  • Per-transaction caps.
  • Whitelists with looser limits for whitelisted destinations.
  • Cooldowns between large transactions.
  • Confirmation delays for transactions above a threshold.
  • Device-fingerprint or geofence requirements on the 2FA channel.

The server refuses to cosign any key-path spend that violates the active policy. Since the policy is user-signed, the server cannot fabricate or relax it unilaterally.

Policy updates require a new user-signed commitment, gated by a configurable policy-change delay (e.g. 7 days). The change-delay value is itself part of the active policy and can be lengthened freely but only shortened slowly — never instantly. This prevents a coercer from immediately relaxing the limits to drain the wallet, at the cost of slightly less convenient legitimate tightening of policy.

Bypass via recovery. A coercer aware of the policy might force the user to initiate unilateral recovery instead. This costs them the full recovery delay — 4+ weeks of hostage time — which is a much stronger deterrent than the no-policy case where a single coerced transaction drains everything.

TEE assurance. If the server runs in a TEE, the user can attest that an unmodified policy engine is enforcing the commitment, removing residual trust in the server operator.


Failure Cases

The table distinguishes cryptographic custody (can the funds ever be recovered?) from operational liveness (does recovery require timely action during a delay window?).

Failure Custody Liveness requirement
User loses key Recoverable via heir after inheritance delay. Heir initiates inheritance; no time pressure on heir.
User loses 2FA factor (not signing key) Recoverable via recovery path after delay. User initiates kickoff and recovers.
Server offline Recoverable via recovery path after delay. Same.
User key lost AND server offline Recoverable via heir. None on user's side.
Attacker has user's key Recoverable iff user is alerted and cancels within recovery delay. Load-bearing. Requires alert delivery + user response within the window.
Attacker has server's key No funds at risk. None. Out-of-protocol social-engineering risk only.
Attacker has both keys Lost. Irreducible 2FA failure.
Physical coercion ($5 wrench) Capped at per-period policy limit. Bypass via forced recovery costs 4+ weeks of hostage time.
Malicious heir Cancellable by user; one re-deposit per heir attempt. User must be alerted and cancel within inheritance delay.
Heir loses key Inheritance unusable until user rotates heir (re-deposit). Detected at heir-onboarding, not runtime.
User loses wallet metadata Keys alone may be insufficient. Back up packets, kickoff templates, and slot maps alongside seeds.
Kickoff fee underpriced Recovery clock can't start until kickoff confirms. Anchor output + working CPFP required.
Address reuse Both UTXOs at the same address share one slot pair; only one is recoverable via the emergency paths. Wallet must enforce single-use; server should refuse change to a used address.

The most consequential operational requirement is alerting the user during the recovery window when an attacker holds their key. The protocol should not be deployed without redundant alert channels and ideally one or more independent watchtowers pre-armed with cancellation transactions.


Security Model

Custody. No single party can move funds. The server has one key in a MuSig2 aggregate and cannot sign alone. The heir's authority is gated by a delay the user can preempt. The user's key alone cannot complete a recovery within the delay window unless alerts fail.

Liveness. The recovery delay is the operational defense against user-key compromise. Within the delay, the user must be reachable through at least one alert channel — or have arranged a watchtower with a pre-signed cancellation transaction. Watchtowers receive no authority beyond the ability to broadcast a fee-burning cancellation, which is exactly the user's own cancellation action.

Server compromise. Strictly weaker than Green: in Green, a compromised server key on a CSV-expired deposit allows direct fund theft. Here, server compromise yields nothing without the user's key.

No refresh treadmill. Recovery and inheritance delays are anchored to kickoff confirmation, not deposit. Deposit UTXOs are stable indefinitely; re-deposits happen only reactively after a cancellation event.

Coercion resistance. Server-enforced spending policies cap an attacker's take from a coerced spend. The policy-change delay prevents instant relaxation under duress.


Extensions

Decaying Multisig

Wizardsardine's Liana popularized decaying multisig: a script where the required signature threshold drops over time (e.g. 3-of-5 today, 2-of-5 after 3 months, 1-of-5 after 1 year). Liana implements this with a CSV that ticks from deposit, so users must refresh coins before the decay activates, or the wallet drops to a lower threshold automatically.

The kickoff-anchored co-input pattern in this protocol applies directly. Instead of one recovery leaf, build a deposit script with one leaf per decay step:

Key path:  k-of-n  (current threshold)
Script path:
  Leaf 1 (co-input: decayUTXO_a, nSequence = 3 months):  (k-1)-of-n
  Leaf 2 (co-input: decayUTXO_b, nSequence = 1 year):    (k-2)-of-n
  ...

Each decayUTXO_x is produced by its own kickoff, gated by a quorum of the current threshold. Activation is opt-in and revocable: the multisig only decays if a quorum initiates it, and any remaining keyholder can cancel by burning the relevant decay token. Funds stay at full threshold indefinitely without refreshes, and decay is available the moment a key is genuinely lost.

This is a strict improvement over deposit-anchored CSV decay for the same reason the recovery path is: timer-from-event, not timer-from-deposit.


Protocol Invariants

  1. The user never deposits without confirmed kickoff roots and a precomputed kickoff template providing both unilateral exit paths.
  2. The server never cosigns a key-path spend without verifying 2FA and the active policy.
  3. Cancellation terminates the entire active segment (all live co-input tokens + next_kickoffRoot) in one transaction.
  4. After cancellation, the user re-deposits to restore the emergency paths.
  5. Each deposit address is used exactly once.

Notes on Implementation

Anchor outputs. Each kickoffTx includes an ephemeral anchor output for CPFP-based fee-bumping. This is essential — kickoffTx's txid is committed in covenants and deposit scripts, so the transaction itself cannot be RBF'd.

Kickoff root funding. Both kickoffRoot and kickoffRoot2 are dust Taproot outputs created in a one-time setup transaction. They must be reserved and never spent for unrelated purposes — the wallet marks them non-spendable for normal use.

Server-side state. The server needs the user's deposit addresses (to watch for kickoff activity), kickoff root outpoints, the user's 2FA secret, and the active policy. None of this is custody-critical; loss degrades alerting and policy enforcement but doesn't endanger funds.

Heir onboarding. The heir receives the static inheritance packet (two xpubs, two chain root outpoints, the heir's own key, delays). Wallets should produce this as a sealed envelope at setup and provide instructions for use.

Multiple alert channels and watchtowers. Strongly recommended. The 4-week (or longer) recovery delay is only a defense if the user — or an agent acting on their behalf — actually notices and responds.


Assumptions

  • OP_TEMPLATEHASH2 is activated as a Tapscript opcode.
  • The user's 2FA factor is held on a device separate from their signing key.
  • The user is reachable through at least one alert channel within the chosen recovery delay, or has arranged a watchtower.
  • The user maintains kickoff roots and does not spend them for unrelated purposes.
  • Fee management on the kickoff anchor is sufficient for the kickoff to confirm.

References

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