Skip to content

Instantly share code, notes, and snippets.

@earonesty
Last active December 18, 2025 23:26
Show Gist options
  • Select an option

  • Save earonesty/ea086aa995be1a860af093f93bd45bf2 to your computer and use it in GitHub Desktop.

Select an option

Save earonesty/ea086aa995be1a860af093f93bd45bf2 to your computer and use it in GitHub Desktop.
demonstration pseudocode for quantum-resistant spends
import hashlib
import json
import os
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple
def sha256(b: bytes) -> bytes:
return hashlib.sha256(b).digest()
def canon(obj) -> bytes:
return json.dumps(obj, sort_keys=True, separators=(",", ":")).encode("utf-8")
def txid_from_parts(body: dict, vin: list, vout: list) -> str:
return sha256(canon({"body": body, "vin": vin, "vout": vout})).hex()
def sighash_from_parts(body: dict, vin: list, vout: list) -> bytes:
return sha256(canon({"body": body, "vin": vin, "vout": vout}))
def ctv_template_hash(tx_body: dict, vin_prevouts: list, vout_scripts: list) -> bytes:
"""
Toy CTV-like hash for this demo:
- excludes witness
- commits to tx body and outputs
- does NOT commit to input outpoint txids (avoids circular dependency when templates are built before txids exist)
"""
vin_shape = [{"vout": i["vout"]} for i in vin_prevouts]
return sha256(canon({"body": tx_body, "vin": vin_shape, "vout": vout_scripts}))
def scriptpubkey_hash(scriptpubkey: dict) -> bytes:
"""
Stand-in for Taproot output key P_anchor.
Real Taproot P_anchor is derived from a NUMS internal key + script tree.
Here we model "scriptPubKey identity" as sha256(canon(scriptpubkey)).
"""
return sha256(canon(scriptpubkey))
@dataclass(frozen=True)
class OutPoint:
txid: str
vout: int
@dataclass(frozen=True)
class TxOut:
value: int
script: dict
@dataclass(frozen=True)
class TxIn:
prevout: OutPoint
witness: dict
@dataclass(frozen=True)
class Tx:
body: dict
vin: List[TxIn]
vout: List[TxOut]
@property
def id(self) -> str:
vin = [{"txid": i.prevout.txid, "vout": i.prevout.vout, "witness": i.witness} for i in self.vin]
vout = [{"value": o.value, "script": o.script} for o in self.vout]
return txid_from_parts(self.body, vin, vout)
@property
def sighash(self) -> bytes:
vin = [{"txid": i.prevout.txid, "vout": i.prevout.vout} for i in self.vin]
vout = [{"value": o.value, "script": o.script} for o in self.vout]
return sighash_from_parts(self.body, vin, vout)
@dataclass(frozen=True)
class Block:
prev: Optional[str]
height: int
txs: List[Tx]
@property
def id(self) -> str:
return sha256(canon({
"prev": self.prev,
"height": self.height,
"txids": [t.id for t in self.txs],
})).hex()
class Chain:
def __init__(self):
self.blocks: List[Block] = []
self.tx_index: Dict[str, Tuple[int, Tx]] = {}
self.utxo: Dict[OutPoint, TxOut] = {}
self.spent: set[OutPoint] = set()
@property
def tip(self) -> Optional[Block]:
return self.blocks[-1] if self.blocks else None
def get_tx(self, tid: str) -> Optional[Tuple[int, Tx]]:
return self.tx_index.get(tid)
def get_tx_height(self, tid: str) -> Optional[int]:
got = self.tx_index.get(tid)
return got[0] if got else None
def add_block(self, txs: List[Tx]) -> Block:
height = len(self.blocks)
blk = Block(prev=self.tip.id if self.tip else None, height=height, txs=txs)
for tx in txs:
self.tx_index[tx.id] = (height, tx)
for tx in txs:
for ti in tx.vin:
if ti.prevout in self.spent:
raise ValueError("double-spend")
prev = self.utxo.get(ti.prevout)
if prev is None:
raise ValueError("missing utxo")
if not self.eval_script(prev, tx, ti, blk.height):
raise ValueError("script eval failed")
self.spent.add(ti.prevout)
del self.utxo[ti.prevout]
for n, out in enumerate(tx.vout):
self.utxo[OutPoint(tx.id, n)] = out
self.blocks.append(blk)
return blk
def eval_script(self, prevout_txout: TxOut, spend_tx: Tx, txin: TxIn, spend_height: int) -> bool:
script = prevout_txout.script
st = script.get("type")
# Phase 0 Funding UTXO: OP_TXHASH-style funnel to P_anchor (NUMS/script-path only) with fee bound.
#
# Lock script:
# {"type":"FUND_LOCK",
# "Cx":hex(H(x)),
# "k":int,
# "P_anchor":hex32, # scriptPubKey hash for the Anchor envelope (Taproot output key analog)
# "max_fee":int, # bound for Phase 0 -> Phase 1
# "nums":True
# }
#
# Spend (Phase 1 AnchorPublishTx) requirements:
# - exactly 1 output
# - output[0].script hash == P_anchor
# - output[0].value >= input_value - max_fee
#
if st == "FUND_LOCK":
if script.get("nums") is not True:
return False
Cx_hex = script["Cx"]
_k = int(script["k"])
P_anchor_hex = script["P_anchor"]
max_fee = int(script.get("max_fee", 0))
if not isinstance(Cx_hex, str) or len(Cx_hex) != 64:
return False
if not isinstance(P_anchor_hex, str) or len(P_anchor_hex) != 64:
return False
if max_fee < 0:
return False
if len(spend_tx.vout) != 1:
return False
out0 = spend_tx.vout[0]
if scriptpubkey_hash(out0.script).hex() != P_anchor_hex:
return False
if out0.value < (prevout_txout.value - max_fee):
return False
return True
# Anchor UTXO: spend2 (reveal) OR escape.
#
# Lock script (Anchor envelope; NUMS/script-path only):
# {"type":"ANCHOR_LOCK","Cx":hex(H(x)),"k":int,"T":hex32,"E":hex32,"nums":True}
#
# spend2 witness (reveal):
# {"mode":"reveal", "x":hexbytes}
# Requires spend_height - anchor_height >= k AND ctv_template_hash(spend_tx) == T
#
# spend2 witness (escape):
# {"mode":"escape"}
# Requires ctv_template_hash(spend_tx) == E ; does NOT reveal x.
#
if st == "ANCHOR_LOCK":
if script.get("nums") is not True:
return False
Cx = bytes.fromhex(script["Cx"])
k = int(script["k"])
T = bytes.fromhex(script["T"])
E = bytes.fromhex(script["E"])
mode = txin.witness.get("mode")
if mode not in ("reveal", "escape"):
return False
anchor_height = self.get_tx_height(txin.prevout.txid)
if anchor_height is None:
return False
vin_prevouts = [{"txid": i.prevout.txid, "vout": i.prevout.vout} for i in spend_tx.vin]
vout_scripts = [{"value": o.value, "script": o.script} for o in spend_tx.vout]
th = ctv_template_hash(spend_tx.body, vin_prevouts, vout_scripts)
if mode == "reveal":
if (spend_height - anchor_height) < k:
return False
x_hex = txin.witness.get("x")
if not isinstance(x_hex, str):
return False
x = bytes.fromhex(x_hex)
if sha256(x) != Cx:
return False
return th == T
return th == E
return False
def make_anchor_script(Cx_hex: str, k: int, T_hex: str, E_hex: str) -> dict:
return {"type": "ANCHOR_LOCK", "Cx": Cx_hex, "k": k, "T": T_hex, "E": E_hex, "nums": True}
def make_funding(value: int, x: bytes, k: int, P_anchor_hex: str, max_fee: int) -> Tuple[Tx, str]:
Cx_hex = sha256(x).hex()
tx = Tx(
body={"type": "FUND"},
vin=[],
vout=[TxOut(
value=value,
script={
"type": "FUND_LOCK",
"Cx": Cx_hex,
"k": k,
"P_anchor": P_anchor_hex,
"max_fee": max_fee,
"nums": True,
},
)],
)
return tx, Cx_hex
def make_anchor_spend1(fund_prevout: OutPoint, value: int, anchor_script: dict) -> Tx:
return Tx(
body={"type": "ANCHOR_SPEND1"},
vin=[TxIn(prevout=fund_prevout, witness={"mode": "publish_anchor"})],
vout=[TxOut(value=value, script=anchor_script)],
)
def make_reveal_spend2(anchor_prevout: OutPoint, value: int, dest_script: dict, x: bytes) -> Tx:
return Tx(
body={"type": "REVEAL_SPEND2"},
vin=[TxIn(prevout=anchor_prevout, witness={"mode": "reveal", "x": x.hex()})],
vout=[TxOut(value=value, script=dest_script)],
)
def make_escape_spend2(anchor_prevout: OutPoint, value: int, escape_dest_script: dict) -> Tx:
return Tx(
body={"type": "ESCAPE_SPEND2"},
vin=[TxIn(prevout=anchor_prevout, witness={"mode": "escape"})],
vout=[TxOut(value=value, script=escape_dest_script)],
)
def build_templates_and_anchor_script(
x: bytes,
k: int,
value: int,
dest: dict,
escape_dest: dict,
) -> Tuple[Tx, Tx, dict, str, str]:
anchor_outp_placeholder = OutPoint("TBD_ANCHOR_TXID", 0)
reveal_template = make_reveal_spend2(anchor_outp_placeholder, value, dest, x)
escape_template = make_escape_spend2(anchor_outp_placeholder, value, escape_dest)
vin_prevouts = [{"txid": i.prevout.txid, "vout": i.prevout.vout} for i in reveal_template.vin]
vout_scripts = [{"value": o.value, "script": o.script} for o in reveal_template.vout]
T_hex = ctv_template_hash(reveal_template.body, vin_prevouts, vout_scripts).hex()
vin_prevouts_e = [{"txid": i.prevout.txid, "vout": i.prevout.vout} for i in escape_template.vin]
vout_scripts_e = [{"value": o.value, "script": o.script} for o in escape_template.vout]
E_hex = ctv_template_hash(escape_template.body, vin_prevouts_e, vout_scripts_e).hex()
Cx_hex = sha256(x).hex()
anchor_script = make_anchor_script(Cx_hex, k, T_hex, E_hex)
return reveal_template, escape_template, anchor_script, T_hex, E_hex
def demo_reveal_flow() -> dict:
chain = Chain()
value = 1000
max_fee = 50
fee = 10
anchor_value = value - fee
x = os.urandom(32)
k = 2
dest = {"type": "DEST", "to": "owner_safe_script_v1"}
escape_dest = {"type": "DEST", "to": "owner_escape_vault_v1"}
_, _, anchor_script, _, _ = build_templates_and_anchor_script(x, k, anchor_value, dest, escape_dest)
P_anchor_hex = scriptpubkey_hash(anchor_script).hex()
fund, _Cx_hex = make_funding(value, x, k, P_anchor_hex, max_fee)
chain.add_block([fund])
fund_outp = OutPoint(fund.id, 0)
spend1 = make_anchor_spend1(fund_outp, anchor_value, anchor_script)
chain.add_block([spend1])
anchor_outp = OutPoint(spend1.id, 0)
early_reveal = make_reveal_spend2(anchor_outp, anchor_value, dest, x)
early_rejected = False
try:
chain.add_block([early_reveal])
except ValueError:
early_rejected = True
chain.add_block([])
thief_dest = {"type": "DEST", "to": "attacker_address"}
thief_rejected = False
try:
chain.add_block([make_reveal_spend2(anchor_outp, anchor_value, thief_dest, x)])
except ValueError:
thief_rejected = True
reveal_ok = make_reveal_spend2(anchor_outp, anchor_value, dest, x)
chain.add_block([reveal_ok])
return {
"k": k,
"max_fee": max_fee,
"fee": fee,
"fund_txid": fund.id,
"spend1_anchor_txid": spend1.id,
"anchor_utxo": {"txid": spend1.id, "vout": 0},
"early_reveal_rejected": early_rejected,
"thief_redirect_rejected": thief_rejected,
"reveal_spend2_txid": reveal_ok.id,
}
def demo_escape_flow() -> dict:
chain = Chain()
value = 1000
max_fee = 50
fee = 10
anchor_value = value - fee
x = os.urandom(32)
k = 2
dest = {"type": "DEST", "to": "owner_normal_dest_v1"}
escape_dest = {"type": "DEST", "to": "owner_escape_vault_v1"}
_, _, anchor_script, _, _ = build_templates_and_anchor_script(x, k, anchor_value, dest, escape_dest)
P_anchor_hex = scriptpubkey_hash(anchor_script).hex()
fund, _Cx_hex = make_funding(value, x, k, P_anchor_hex, max_fee)
chain.add_block([fund])
fund_outp = OutPoint(fund.id, 0)
spend1 = make_anchor_spend1(fund_outp, anchor_value, anchor_script)
chain.add_block([spend1])
anchor_outp = OutPoint(spend1.id, 0)
escape_ok = make_escape_spend2(anchor_outp, anchor_value, escape_dest)
chain.add_block([escape_ok])
return {
"k": k,
"max_fee": max_fee,
"fee": fee,
"fund_txid": fund.id,
"spend1_anchor_txid": spend1.id,
"anchor_utxo": {"txid": spend1.id, "vout": 0},
"escape_spend2_txid": escape_ok.id,
}
if __name__ == "__main__":
print(json.dumps({
"reveal_flow": demo_reveal_flow(),
"escape_flow": demo_escape_flow(),
}, indent=2))
# Anchor-gated, UTXO-moving, template-bound spend
## using OP_TXHASH + OP_CTV with an escape hatch
*(prunable-friendly; quantum-resilient to signature forgery)*
---
## Assumptions
- **OP_CHECKTEMPLATEVERIFY (OP_CTV)** is available per BIP119.
The 32-byte template hash is `DefaultCheckTemplateVerifyHash`.
https://bips.dev/119/
- **OP_TXHASH / OP_CHECKTXHASHVERIFY** is available per the current draft proposal,
allowing scripts to hash and verify selected fields of the *spending transaction*
without committing to a full transaction template.
- **Taproot key-path spending is disabled via NUMS internal keys.**
All Taproot outputs used in this construction MUST use a Nothing-Up-My-Sleeve
(NUMS) internal key, forcing execution through the script path.
If a real internal key is used, a future quantum attacker could derive the
private key and bypass all script enforcement.
- **Relative timelocks** exist (BIP68 / BIP112).
- **SHA256 preimage resistance holds**, even if ECDSA/Schnorr signatures become forgeable.
- Bitcoin nodes do not maintain a historical `txid → transaction` index by default.
---
## Threat model
An attacker may:
- Forge signatures.
- Intercept, delay, reorder, or fee-bump transactions.
- Front-run in the mempool.
- Exploit shallow reorgs.
An attacker may **not**:
- Break SHA256 preimage resistance.
- Violate OP_CTV semantics.
- Violate OP_TXHASH semantics.
- Violate relative timelock rules.
- Rewrite deep chain history.
---
## High-level idea
This construction creates a **multi-phase envelope** that separates:
- *who can trigger execution* from
- *where value is allowed to go*.
Even if signatures are forgeable, funds can only move into a protected
Anchor envelope, and from there only along template-bound paths.
- **Phase 0** funnels all value into a predetermined Anchor envelope.
- **Phase 1** instantiates that envelope on-chain.
- **Phase 2** either:
- reveals a one-time secret to complete a template-bound spend, or
- uses an escape hatch without revealing the secret.
At no point does Phase 0 commit to final recipients.
---
## Data definitions
- **x**: one-time secret (recommended 32 bytes).
- **C = SHA256(x)**.
- **k**: `uint32` relative confirmation depth parameter.
- **T**: 32-byte CTV template hash for the intended reveal spend.
- **E**: 32-byte CTV template hash for the escape-hatch spend.
- **P_anchor**: Taproot output key (with NUMS internal key) committing to an
Anchor script tree that:
- embeds `C`,
- enforces *reveal-or-escape* spending conditions,
- optionally enforces a relative timelock `k` on the reveal path.
---
## Transactions and scripts
### Phase 0: Funding coin (initial UTXO)
**Purpose:**
Ensure that, even in a future where signatures are forgeable, all value must
enter the Anchor envelope and cannot be redirected elsewhere.
#### Phase 0 locking policy
The Phase 0 UTXO enforces the following:
1. **Anchor pinning**
Any spend MUST create exactly one value-bearing output whose
`scriptPubKey` equals `P_anchor`.
2. **No value leakage**
No other value-bearing outputs are permitted.
Transaction fees are paid by reducing the Anchor output amount.
3. **Fee bound**
The Phase 0 script MUST enforce a bound on fee extraction, e.g.:
```
AnchorValue ≥ InputValue − MaxFee
```
This prevents an attacker from draining value via excessive fees while
preserving fee flexibility.
These conditions are enforced using **OP_TXHASH**, selecting and verifying:
- the number of outputs,
- the `scriptPubKey` of the Anchor output,
- and sufficient value information to enforce the fee bound.
No commitment is made to final recipients or future templates.
---
### Phase 1: AnchorPublishTx
**Properties:**
- Spends the Phase 0 UTXO.
- Creates exactly one output: the **Anchor UTXO**, locked to `P_anchor`.
The Anchor envelope is now instantiated on-chain.
An attacker may have triggered this spend, but cannot redirect value.
---
### Anchor UTXO locking script
A Taproot script tree with two spending paths.
#### Path 1: Reveal spend (normal)
Conditions:
1. **Relative depth gate**
The Anchor UTXO must have aged by at least `k` blocks (CSV).
2. **Reveal check**
`SHA256(x) == C`.
3. **Template enforcement**
The spending transaction MUST match template `T` via OP_CTV.
---
#### Path 2: Escape hatch
Conditions:
1. **Template enforcement**
The spending transaction MUST match template `E` via OP_CTV.
2. **No secret revealed**
The value `x` is not disclosed on this path.
The escape path may be immediately available or time-delayed,
depending on the chosen policy.
---
### Phase 2: SpendAnchorTx
- **Reveal path witness:** `x` plus any required non-cryptographic data.
- **Escape path witness:** no `x`.
---
## Security properties
- **Quantum signature safety**
Forged signatures do not enable theft. All value is confined to the Anchor
envelope before any secret is revealed.
- **No redirect-after-reveal**
Once `x` is revealed, OP_CTV pins the outputs. An interceptor cannot redirect
value.
- **Observation is sufficient**
If an attacker publishes Phase 0 or Phase 1 spends, the Anchor script still
contains a usable escape hatch.
- **Reorg resistance**
The relative timelock `k` mitigates shallow reorg games at the reveal boundary.
- **Prunable-friendly**
Validation requires no historical transaction lookup.
- **Graceful degradation**
A quantum attacker can force execution or cause delay, but cannot steal value.
---
## Where OP_TXHASH fits
OP_TXHASH is used **only in Phase 0** to enforce a *partial covenant*:
- it pins the **next envelope** (`P_anchor`),
- without pinning final recipients,
- without pinning templates `T` or `E`,
- without committing to exact output amounts.
This is the minimal constraint required to survive a future where
signatures are forgeable.
Because the destination is pinned only to the Anchor envelope,
Replace-By-Fee (RBF) is safe to allow in Phase 0.
---
## Summary
Phase 0 pins the **envelope**, not the destination.
Phase 1 instantiates the envelope.
Phase 2 chooses and enforces the destination.
The attacker is reduced from a thief to a griefer.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment