-
-
Save earonesty/ea086aa995be1a860af093f93bd45bf2 to your computer and use it in GitHub Desktop.
demonstration pseudocode for quantum-resistant spends
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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