From b24c5049ed9b71aa64cf38fe221f3af2c3b0aee8 Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Fri, 22 Aug 2025 18:42:05 -0400 Subject: [PATCH 01/13] BIP: OP_TWEAKADD --- bip-XXXX.md | 244 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 bip-XXXX.md diff --git a/bip-XXXX.md b/bip-XXXX.md new file mode 100644 index 0000000000..2f0274a481 --- /dev/null +++ b/bip-XXXX.md @@ -0,0 +1,244 @@ +``` +BIP: TBD +Layer: Consensus (soft fork) +Title: OP_TWEAKADD - x-only key tweak addition +Author: Jeremy Rubin +Status: Draft +Type: Standards Track +Created: 2025-08-22 +License: BSD-3-Clause +``` +## Abstract + +This proposal defines a new tapscript opcode, `OP_TWEAKADD`, that takes an x-only public key and a 32-byte integer `h` on the stack and pushes the x-only public key corresponding to `P + h*G`, where `P` is the lifted point for the input x-coordinate and `G` is the secp256k1 generator. The operation mirrors the Taproot tweak used by BIP340 signers and enables simple, verifiable key modifications inside script without revealing private keys or relying on hash locks. + +## Motivation + +Bitcoin already leverages x-only key tweaking (for example, Taproot internal to output key derivation). Exposing a minimal, consensus-enforced version of "add a generator multiple to this key" inside tapscript: + +- Enables script-level key evolutions (e.g., variable dependent authorized keys) without full signature verification at each step. +- Supports scriptless-script patterns where spending conditions are realized by transforming keys rather than revealing preimages. +- Allows compact covenant-like constructions where authorization is carried by key lineage, while keeping semantics narrowly scoped. + + +## Specification + +### Applicability and opcode number + +- Context: Only valid in tapscript (witness version 1, leaf version 0xc0). In legacy or segwit v0 script, `OP_TWEAKADD` is disabled and causes script failure. +- Opcode: OP_TWEAKADD (0xBE, or TBD, any unused OP_SUCCESSx, preferably one which might never be restored in the future). + +### Stack semantics + +Input (top last): + +``` + +... \[pubkey32] \[h32] OP\_TWEAKADD -> ... \[pubkey32\_out] + +```` + +- `pubkey32`: 32-byte x-only public key (big-endian x coordinate). +- `h32`: 32-byte big-endian unsigned integer `t`. + +Output: + +- `pubkey32_out`: 32-byte x-only public key for `Q = P + t*G`. + +### Operation and failure conditions + +Let `n` be the secp256k1 curve order. + +1. Parse `h32` as big-endian integer `t`. If `t >= n`, fail. +2. Interpret `pubkey32` as an x-coordinate and attempt the BIP340 even-Y lift: + - If no curve point exists with that x, fail. + - Otherwise, obtain `P` with even Y. +3. Compute `Q = P + t*G`. If `Q` is the point at infinity, fail. +4. Push `x(Q)` as a 32-byte big-endian value. + +### Conventions + +- X-only keys follow BIP340 conventions (even-Y). +- Scalars must be exactly 32 bytes, big-endian, and less than `n`. +- Non-32-byte inputs fail (consensus). Minimal push rules apply (policy). + +### Resource usage + +- Performs one fixed-base EC scalar multiplication (`t*G`) plus one EC point addition (`P + t*G`). +- Costs should be aligned with `OP_CHECKSIG` operation, budget is decremented by 50. + +## Rationale + +- Even-Y x-only is consistent with BIP340/Taproot. +- Infinity outputs are rejected to avoid invalid keys. +- Functionality is narrowly scoped to Taproot-style tweaks, avoiding arbitrary EC arithmetic. +- Push opcode rather than verification opcode for script compactness. + +## Backwards compatibility + +- Old nodes: treat unknown tapscript opcode as OP_SUCCESSx. +- This is a soft-fork change, tapscript-only. + +## Future compatibility + +- A future OP_CAT or OP_TAPTREE opcode can prepare a tweak for a taproot output key correctly + +## Deployment + +TBD + +## Security considerations + +- Scalar range check prevents overflow and ambiguity. +- Infinity guard ensures valid outputs only. +- Scripts must control `t` derivation securely, which in many applications is trivial. +- No new witness malleability introduced because tweaks must be exactly 32-bytes, and x-only key can only derive one even-Y point. + +## Reference semantics (pseudocode) + +```python +SECP256K1_ORDER = n # 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 + +def tweak_add(pubkey32: bytes, h32: bytes) -> bytes: + if len(pubkey32) != 32 or len(h32) != 32: + raise ValueError + t = int.from_bytes(h32, 'big') + if t >= SECP256K1_ORDER: + raise ValueError + P = lift_x_even_y(pubkey32) # BIP340 lift of x to the point with even Y + if P is None: + raise ValueError + Q = point_add(P, scalar_mul_G(t)) # Q = P + t*G + if Q is None: # point at infinity + raise ValueError + return Q.x.to_bytes(32, 'big') +```` + +## Script evaluation rules + +0. If less than 2 stack elements, fail. +1. Pop `h32`, then `pubkey32`. +2. If either length is not 32, fail. +3. Run `tweak_add` as above. +4. Push the 32-byte x-only result. + +## Test vectors (numeric, hex) + +All values are 32-byte hex, big-endian. Curve is secp256k1 with generator G. Order `n`: + +``` +n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +``` + +The following vectors assume BIP340 even-Y lifting of input x-only keys. + +TODO: these test vectors will be actually computed and checked... + +### Known inputs + +``` +x(G) = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +x(2G) = c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 +x(3G) = f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9 +x(5G) = 2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4 +x(6G) = fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556 +x(7G) = 5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc +``` + +### Passing cases + +1. Identity tweak (t = 0): + +``` +pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +h32 = 0000000000000000000000000000000000000000000000000000000000000000 +result = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +``` + +2. Increment by 1: + +``` +pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +h32 = 0000000000000000000000000000000000000000000000000000000000000001 +result = c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 +``` + +3. Increment by 2: + +``` +pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +h32 = 0000000000000000000000000000000000000000000000000000000000000002 +result = f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9 +``` + +4. Increment by 5: + +``` +pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +h32 = 0000000000000000000000000000000000000000000000000000000000000005 +result = fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556 +``` + +5. Different input x (using x(2G)) with t = 3: + +``` +pubkey32 = c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 +h32 = 0000000000000000000000000000000000000000000000000000000000000003 +result = 2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4 +``` + +6. Larger values: input x(7G) with t = 9: + +``` +pubkey32 = 5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc +h32 = 0000000000000000000000000000000000000000000000000000000000000009 +result = e60fce93b59e9ec53011aabc21c23e97b2a31369b87a5ae9c44ee89e2a6dec0a +``` + +### Failing cases + +A) Scalar out of range (t = n): + +``` +pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +h32 = ffffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 +expect = fail +``` + +B) Invalid x (no lift possible), example x = 0: + +``` +pubkey32 = 0000000000000000000000000000000000000000000000000000000000000000 +h32 = 0000000000000000000000000000000000000000000000000000000000000001 +expect = fail +``` + +C) Infinity result: choose input x(G), t = n - 1 (so P + t*G = n*G = infinity): + +``` +pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +h32 = ffffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140 +expect = fail +``` + + +## Reference implementation notes + +* Reuse BIP340 lift/encode helpers from Taproot verification. +* Implement `t*G` via fixed-base multiplication, then combine with `P` using point addition. +* Serialize the result as 32-byte x-only. +* Charge EC op budget as 50, like `OP_CHECKSIGADD`. + + +## Acknowledgements + +This proposal extends the Taproot tweak mechanism (BIP340/341) into script, inspired by prior work on scriptless scripts and key-evolution constructions. There has been various discussion of OP_TWEAKADD over the years, including by Russell O'Connor and Steven Roose. + +## References + +- [CATT: Thoughts about an alternative covenant softfork proposal](https://delvingbitcoin.org/t/catt-thoughts-about-an-alternative-covenant-softfork-proposal/125) +- [Bitcoindev mailing list discussion](https://gnusha.org/pi/bitcoindev/e98d76f2-6f2c-9c3a-6a31-bccb34578c31@roose.io/) +- [Advent 8: Scriptless Scripts and Key Tweaks](https://rubin.io/bitcoin/2021/12/05/advent-8/) +- [Re: [bitcoin-dev] Unlimited covenants, was Re: CHECKSIGFROMSTACK/{Verify} BIP for Bitcoin](https://gnusha.org/pi/bitcoindev/CAMZUoKnVLRLgL1rcq8DYHRjM--8VEUC5kjUbzbY5S860QSbk5w@mail.gmail.com/) +- [Re: [bitcoin-dev] Unlimited covenants, was Re: CHECKSIGFROMSTACK/{Verify} BIP for Bitcoin](https://gnusha.org/pi/bitcoindev/CAMZUoKkAUodCT+2aQG71xwHYD8KXeTAdQq4NmXZ4GBe0pcD=9A@mail.gmail.com/) +- [ElementsProject: Tapscript opcodes documentation](https://github.com/ElementsProject/elements/blob/master/doc/tapscript_opcodes.md#new-opcodes-for-additional-functionality) From 04f8c61905417764acc38765c543b52b5e627ff6 Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Sat, 23 Aug 2025 12:50:35 -0400 Subject: [PATCH 02/13] BIP TweakAdd: note on commutativity of tweaking and add test cases --- bip-XXXX.md | 185 +++++++++++++++++--------- bip-tweakadd/test-vectors/Cargo.toml | 9 ++ bip-tweakadd/test-vectors/src/main.rs | 172 ++++++++++++++++++++++++ 3 files changed, 305 insertions(+), 61 deletions(-) create mode 100644 bip-tweakadd/test-vectors/Cargo.toml create mode 100644 bip-tweakadd/test-vectors/src/main.rs diff --git a/bip-XXXX.md b/bip-XXXX.md index 2f0274a481..2552fc7b85 100644 --- a/bip-XXXX.md +++ b/bip-XXXX.md @@ -123,105 +123,132 @@ def tweak_add(pubkey32: bytes, h32: bytes) -> bytes: 4. Push the 32-byte x-only result. ## Test vectors (numeric, hex) +Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 -All values are 32-byte hex, big-endian. Curve is secp256k1 with generator G. Order `n`: -``` -n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 -``` - -The following vectors assume BIP340 even-Y lifting of input x-only keys. - -TODO: these test vectors will be actually computed and checked... +### Passing cases -### Known inputs +1) Identity tweak (t = 0) +``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + h32 = 0000000000000000000000000000000000000000000000000000000000000000 + expect = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> <0000000000000000000000000000000000000000000000000000000000000000> OP_TWEAKADD <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_EQUAL ``` -x(G) = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 -x(2G) = c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 -x(3G) = f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9 -x(5G) = 2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4 -x(6G) = fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556 -x(7G) = 5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc +2) Increment by 1 ``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + h32 = 0000000000000000000000000000000000000000000000000000000000000001 + expect = c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 -### Passing cases - -1. Identity tweak (t = 0): - + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD OP_EQUAL ``` -pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 -h32 = 0000000000000000000000000000000000000000000000000000000000000000 -result = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 +3) Increment by 2 ``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + h32 = 0000000000000000000000000000000000000000000000000000000000000002 + expect = f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9 -2. Increment by 1: - + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> <0000000000000000000000000000000000000000000000000000000000000002> OP_TWEAKADD OP_EQUAL ``` -pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 -h32 = 0000000000000000000000000000000000000000000000000000000000000001 -result = c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 +4) Increment by 5 ``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + h32 = 0000000000000000000000000000000000000000000000000000000000000005 + expect = fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556 -3. Increment by 2: + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> <0000000000000000000000000000000000000000000000000000000000000005> OP_TWEAKADD OP_EQUAL +``` +5) Input x(2G), t = 3 +``` + pubkey32 = c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 + h32 = 0000000000000000000000000000000000000000000000000000000000000003 + expect = 2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4 + script = <0000000000000000000000000000000000000000000000000000000000000003> OP_TWEAKADD <2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4> OP_EQUAL ``` -pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 -h32 = 0000000000000000000000000000000000000000000000000000000000000002 -result = f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9 +6) Input x(7G), t = 9 ``` + pubkey32 = 5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc + h32 = 0000000000000000000000000000000000000000000000000000000000000009 + expect = e60fce93b59e9ec53011aabc21c23e97b2a31369b87a5ae9c44ee89e2a6dec0a -4. Increment by 5: + script = <5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc> <0000000000000000000000000000000000000000000000000000000000000009> OP_TWEAKADD OP_EQUAL +``` +7) Input x(h(1) G), t = 1 +``` + pubkey32 = d415b187c6e7ce9da46ac888d20df20737d6f16a41639e68ea055311e1535dd9 + h32 = 0000000000000000000000000000000000000000000000000000000000000001 + expect = c6713b2ac2495d1a879dc136abc06129a7bf355da486cd25f757e0a5f6f40f74 + script = <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD OP_EQUAL ``` -pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 -h32 = 0000000000000000000000000000000000000000000000000000000000000005 -result = fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556 +8) Input x(h(2) G), t = 1 ``` + pubkey32 = d27cd27dbff481bc6fc4aa39dd19405eb6010237784ecba13bab130a4a62df5d + h32 = 0000000000000000000000000000000000000000000000000000000000000001 + expect = 136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785 -5. Different input x (using x(2G)) with t = 3: + script = <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD <136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785> OP_EQUAL +``` +9) Input x(h(7) G), t = 1 +``` + pubkey32 = ddc399701a78edd5ea56429b2b7b6cd11f7d1e4015e7830b4f5e07eb25058768 + h32 = 0000000000000000000000000000000000000000000000000000000000000001 + expect = 0e27b02714b3f2344f2bfa6d821654f2bd9f0ef497ec541b653b8dcb3a915faf + script = <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD <0e27b02714b3f2344f2bfa6d821654f2bd9f0ef497ec541b653b8dcb3a915faf> OP_EQUAL ``` -pubkey32 = c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 -h32 = 0000000000000000000000000000000000000000000000000000000000000003 -result = 2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4 +10) Input x(G), t = 1 ``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + h32 = 4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a + expect = c6713b2ac2495d1a879dc136abc06129a7bf355da486cd25f757e0a5f6f40f74 -6. Larger values: input x(7G) with t = 9: + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> <4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a> OP_TWEAKADD OP_EQUAL +``` +11) Input x(G), t = h(2) +``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + h32 = dbc1b4c900ffe48d575b5da5c638040125f65db0fe3e24494b76ea986457d986 + expect = 136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785 + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD <136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785> OP_EQUAL ``` -pubkey32 = 5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc -h32 = 0000000000000000000000000000000000000000000000000000000000000009 -result = e60fce93b59e9ec53011aabc21c23e97b2a31369b87a5ae9c44ee89e2a6dec0a +12) Input x(G), t = h(7) (Note: differs from 9) ``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + h32 = ca358758f6d27e6cf45272937977a748fd88391db679ceda7dc7bf1f005ee879 + expect = 00b152fb17d249541e3b2f51455269e02d76507ad7857aaa98e3c51ee5da5b1d -### Failing cases + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD <00b152fb17d249541e3b2f51455269e02d76507ad7857aaa98e3c51ee5da5b1d> OP_EQUAL +``` -A) Scalar out of range (t = n): +### Failing cases +A) Scalar out of range (t = n) ``` -pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 -h32 = ffffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 -expect = fail + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + h32 = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 + expect = fail + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_DROP OP_1 ``` - -B) Invalid x (no lift possible), example x = 0: - +B) Invalid x (x = 0), t = 1 ``` -pubkey32 = 0000000000000000000000000000000000000000000000000000000000000000 -h32 = 0000000000000000000000000000000000000000000000000000000000000001 -expect = fail + pubkey32 = 0000000000000000000000000000000000000000000000000000000000000000 + h32 = 0000000000000000000000000000000000000000000000000000000000000001 + expect = fail + script = <0000000000000000000000000000000000000000000000000000000000000000> <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD OP_DROP OP_1 ``` - -C) Infinity result: choose input x(G), t = n - 1 (so P + t*G = n*G = infinity): - +C) Infinity result (x(G), t = n-1) ``` -pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 -h32 = ffffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140 -expect = fail + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + h32 = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140 + expect = fail + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_DROP OP_1 ``` - ## Reference implementation notes * Reuse BIP340 lift/encode helpers from Taproot verification. @@ -230,6 +257,42 @@ expect = fail * Charge EC op budget as 50, like `OP_CHECKSIGADD`. +## Protocol Design Note: Scalar Adjustment + +When working with x-only keys, it is important to remember that each 32-byte value encodes the equivalence class `{P, −P}`. +BIP340 defines the canonical lift as **the point with even Y**. As a result: + +- If an off-chain protocol describes an x-only key as "the point `s·G`," then in consensus terms the actual key is `adj(s)·G`, where: + +``` + +adj(s) = s if y(s·G) is even + = n − s if y(s·G) is odd + +``` + +- Consequently, `OP_TWEAKADD(x(s·G), t)` always computes: + +``` + +result = x(adj(s)·G + t·G) + +``` + +not simply `x(s·G + t·G)`. + +This distinction is invisible when signing or verifying against BIP340 keys, because both `s` and `n − s` yield the same x-only key. +But it matters when a protocol tries to relate "a tweak applied at the base" (`x(G), t = s`) to "a tweak applied at a derived key" (`x(s·G), t = 1`). In general those will differ unless the original point already had even Y. + + +- If you want consistent algebraic relations across different ways of composing tweaks, **normalize scalars off-chain** before pushing them into script. +- That is: replace every candidate tweak `s` with `adj(s)`, so that `adj(s)·G` has even Y. +- A simple library function can perform this parity check and adjustment using libsecp256k1; it does require a consensus modification or opcode. + +If the tweak is derived from inflexible state, such as a transaction hash or a signature, it may be infeasible to depend on commutativity of tweaking. + + + ## Acknowledgements This proposal extends the Taproot tweak mechanism (BIP340/341) into script, inspired by prior work on scriptless scripts and key-evolution constructions. There has been various discussion of OP_TWEAKADD over the years, including by Russell O'Connor and Steven Roose. diff --git a/bip-tweakadd/test-vectors/Cargo.toml b/bip-tweakadd/test-vectors/Cargo.toml new file mode 100644 index 0000000000..cc27f5e697 --- /dev/null +++ b/bip-tweakadd/test-vectors/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "test-vectors" +version = "0.1.0" +edition = "2024" + +[dependencies] +secp256k1 = "0.29" +hex = "0.4" +bitcoin_hashes = "0.16.0" diff --git a/bip-tweakadd/test-vectors/src/main.rs b/bip-tweakadd/test-vectors/src/main.rs new file mode 100644 index 0000000000..f19a5d1fd3 --- /dev/null +++ b/bip-tweakadd/test-vectors/src/main.rs @@ -0,0 +1,172 @@ +use secp256k1::{constants::CURVE_ORDER, PublicKey, Secp256k1, SecretKey, XOnlyPublicKey}; + +fn hex32(b: &[u8; 32]) -> String { + b.iter().map(|x| format!("{:02x}", x)).collect() +} + +/// Implements OP_TWEAKADD semantics. +/// Returns None if invalid scalar, invalid x, or infinity. +fn tweak_add_xonly(pubkey32: [u8; 32], h32: [u8; 32]) -> Option<[u8; 32]> { + let secp = Secp256k1::new(); + + // Reject if t >= n + let scalar = secp256k1::Scalar::from_be_bytes(h32).ok()?; + // Lift pubkey from x-only + let xpk = XOnlyPublicKey::from_slice(&pubkey32).ok()?; + let (xonly, _) = xpk.add_tweak(&secp, &scalar).ok()?; + Some(xonly.serialize()) +} + +fn case(name: &str, pubkey_hex: &str, t_hex: &str, check_res: Option<&str>) { + let pk_bytes = hex::decode(pubkey_hex).unwrap(); + let t_bytes = hex::decode(t_hex).unwrap(); + let mut pk32 = [0u8; 32]; + pk32.copy_from_slice(&pk_bytes); + let mut t32 = [0u8; 32]; + t32.copy_from_slice(&t_bytes); + match tweak_add_xonly(pk32, t32) { + Some(out) => { + let out_hex = hex32(&out); + if let Some(check) = check_res { + assert_eq!(out_hex, check); + } + + let script = format!("<{pubkey_hex}> <{t_hex}> OP_TWEAKADD <{out_hex}> OP_EQUAL"); + + println!("{name}\n```\n pubkey32 = {pubkey_hex}\n h32 = {t_hex}\n expect = {out_hex}\n\n script = {script}\n```") + } + None => { + let script = format!("<{pubkey_hex}> <{t_hex}> OP_TWEAKADD OP_DROP OP_1"); + println!("{name}\n```\n pubkey32 = {pubkey_hex}\n h32 = {t_hex}\n expect = fail\n script = {script}\n```") + } + } +} + +/// Helper: compute x-only for scalar*k*G. +fn xonly_of_scalar(k: u8) -> String { + let secp = Secp256k1::new(); + let mut buf = [0u8; 32]; + buf[31] = k; + let sk = SecretKey::from_slice(&buf).unwrap(); + let pk = PublicKey::from_secret_key(&secp, &sk); + let (xonly, _) = pk.x_only_public_key(); + hex32(&xonly.serialize()) +} + +fn hash_scalar(k: u8) -> [u8; 32] { + bitcoin_hashes::Sha256::hash(&[k]).to_byte_array() +} +/// Helper: compute x-only for scalar*k*G. +fn xonly_of_scalar_hash(k: [u8; 32]) -> String { + let secp = Secp256k1::new(); + let sk = SecretKey::from_slice(&k).unwrap(); + let pk = PublicKey::from_secret_key(&secp, &sk); + let (xonly, _) = pk.x_only_public_key(); + hex32(&xonly.serialize()) +} + +fn main() { + println!("Curve order n = {}", hex::encode(CURVE_ORDER)); + println!(); + + let x_g = xonly_of_scalar(1); + let x_2g = xonly_of_scalar(2); + let x_3g = xonly_of_scalar(3); + let x_5g = xonly_of_scalar(5); + let x_6g = xonly_of_scalar(6); + let x_7g = xonly_of_scalar(7); + let x_16g = xonly_of_scalar(16); + + let h1 = hash_scalar(1); + let h2 = hash_scalar(2); + let h7 = hash_scalar(7); + let x_h1g = xonly_of_scalar_hash(h1); + let x_h2g = xonly_of_scalar_hash(h2); + let x_h7g = xonly_of_scalar_hash(h7); + + println!("\n### Passing cases\n"); + case( + "1) Identity tweak (t = 0)", + &x_g, + "0000000000000000000000000000000000000000000000000000000000000000", + Some(&x_g), + ); + case( + "2) Increment by 1", + &x_g, + "0000000000000000000000000000000000000000000000000000000000000001", + Some(&x_2g), + ); + case( + "3) Increment by 2", + &x_g, + "0000000000000000000000000000000000000000000000000000000000000002", + Some(&x_3g), + ); + case( + "4) Increment by 5", + &x_g, + "0000000000000000000000000000000000000000000000000000000000000005", + Some(&x_6g), + ); + case( + "5) Input x(2G), t = 3", + &x_2g, + "0000000000000000000000000000000000000000000000000000000000000003", + Some(&x_5g), + ); + case( + "6) Input x(7G), t = 9", + &x_7g, + "0000000000000000000000000000000000000000000000000000000000000009", + Some(&x_16g), + ); + + case( + "7) Input x(h(1) G), t = 1", + &x_h1g, + "0000000000000000000000000000000000000000000000000000000000000001", + None, + ); + case( + "8) Input x(h(2) G), t = 1", + &x_h2g, + "0000000000000000000000000000000000000000000000000000000000000001", + None, + ); + case( + "9) Input x(h(7) G), t = 1", + &x_h7g, + "0000000000000000000000000000000000000000000000000000000000000001", + None, + ); + + case("10) Input x(G), t = 1", &x_g, &hex32(&h1), None); + case("11) Input x(G), t = h(2)", &x_g, &hex32(&h2), None); + case( + "12) Input x(G), t = h(7) (Note: differs from 9)", + &x_g, + &hex32(&h7), + None, + ); + + println!("\n### Failing cases\n"); + case( + "A) Scalar out of range (t = n)", + &x_g, + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + None, + ); + case( + "B) Invalid x (x = 0), t = 1", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000001", + None, + ); + case( + "C) Infinity result (x(G), t = n-1)", + &x_g, + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140", + None, + ); +} From 601d45f16cba541b30c33bd8967718e019767bba Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Sat, 23 Aug 2025 12:57:42 -0400 Subject: [PATCH 03/13] BIP TweakAdd: Invert Argument Order --- bip-XXXX.md | 56 +++++++++++++++------------ bip-tweakadd/test-vectors/src/main.rs | 4 +- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/bip-XXXX.md b/bip-XXXX.md index 2552fc7b85..7a8f78c35c 100644 --- a/bip-XXXX.md +++ b/bip-XXXX.md @@ -34,18 +34,18 @@ Input (top last): ``` -... \[pubkey32] \[h32] OP\_TWEAKADD -> ... \[pubkey32\_out] +... \[h32] \[pubkey32] OP\_TWEAKADD -> ... \[pubkey32\_out] ```` -- `pubkey32`: 32-byte x-only public key (big-endian x coordinate). - `h32`: 32-byte big-endian unsigned integer `t`. +- `pubkey32`: 32-byte x-only public key (big-endian x coordinate). Output: - `pubkey32_out`: 32-byte x-only public key for `Q = P + t*G`. -### Operation and failure conditions +#### Operation and failure conditions Let `n` be the secp256k1 curve order. @@ -56,6 +56,16 @@ Let `n` be the secp256k1 curve order. 3. Compute `Q = P + t*G`. If `Q` is the point at infinity, fail. 4. Push `x(Q)` as a 32-byte big-endian value. +Note: `t = 0` may fail if `pubkey32` is not valid. + +#### Script evaluation rules + +0. If less than 2 stack elements, fail. +1. Pop `pubkey32` and then `h32` +2. If either length is not 32, fail. +3. Run `tweak_add` as above. +4. Push the 32-byte x-only result. + ### Conventions - X-only keys follow BIP340 conventions (even-Y). @@ -73,6 +83,7 @@ Let `n` be the secp256k1 curve order. - Infinity outputs are rejected to avoid invalid keys. - Functionality is narrowly scoped to Taproot-style tweaks, avoiding arbitrary EC arithmetic. - Push opcode rather than verification opcode for script compactness. +- Argument order to permit tweak from witness onto fixed key without OP_SWAP. ## Backwards compatibility @@ -114,15 +125,10 @@ def tweak_add(pubkey32: bytes, h32: bytes) -> bytes: return Q.x.to_bytes(32, 'big') ```` -## Script evaluation rules -0. If less than 2 stack elements, fail. -1. Pop `h32`, then `pubkey32`. -2. If either length is not 32, fail. -3. Run `tweak_add` as above. -4. Push the 32-byte x-only result. +## Test vectors (Generated) + -## Test vectors (numeric, hex) Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 @@ -134,7 +140,7 @@ Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 h32 = 0000000000000000000000000000000000000000000000000000000000000000 expect = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 - script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> <0000000000000000000000000000000000000000000000000000000000000000> OP_TWEAKADD <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_EQUAL + script = <0000000000000000000000000000000000000000000000000000000000000000> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_EQUAL ``` 2) Increment by 1 ``` @@ -142,7 +148,7 @@ Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 h32 = 0000000000000000000000000000000000000000000000000000000000000001 expect = c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 - script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD OP_EQUAL + script = <0000000000000000000000000000000000000000000000000000000000000001> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_EQUAL ``` 3) Increment by 2 ``` @@ -150,7 +156,7 @@ Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 h32 = 0000000000000000000000000000000000000000000000000000000000000002 expect = f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9 - script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> <0000000000000000000000000000000000000000000000000000000000000002> OP_TWEAKADD OP_EQUAL + script = <0000000000000000000000000000000000000000000000000000000000000002> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_EQUAL ``` 4) Increment by 5 ``` @@ -158,7 +164,7 @@ Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 h32 = 0000000000000000000000000000000000000000000000000000000000000005 expect = fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556 - script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> <0000000000000000000000000000000000000000000000000000000000000005> OP_TWEAKADD OP_EQUAL + script = <0000000000000000000000000000000000000000000000000000000000000005> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_EQUAL ``` 5) Input x(2G), t = 3 ``` @@ -166,7 +172,7 @@ Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 h32 = 0000000000000000000000000000000000000000000000000000000000000003 expect = 2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4 - script = <0000000000000000000000000000000000000000000000000000000000000003> OP_TWEAKADD <2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4> OP_EQUAL + script = <0000000000000000000000000000000000000000000000000000000000000003> OP_TWEAKADD <2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4> OP_EQUAL ``` 6) Input x(7G), t = 9 ``` @@ -174,7 +180,7 @@ Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 h32 = 0000000000000000000000000000000000000000000000000000000000000009 expect = e60fce93b59e9ec53011aabc21c23e97b2a31369b87a5ae9c44ee89e2a6dec0a - script = <5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc> <0000000000000000000000000000000000000000000000000000000000000009> OP_TWEAKADD OP_EQUAL + script = <0000000000000000000000000000000000000000000000000000000000000009> <5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc> OP_TWEAKADD OP_EQUAL ``` 7) Input x(h(1) G), t = 1 ``` @@ -182,7 +188,7 @@ Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 h32 = 0000000000000000000000000000000000000000000000000000000000000001 expect = c6713b2ac2495d1a879dc136abc06129a7bf355da486cd25f757e0a5f6f40f74 - script = <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD OP_EQUAL + script = <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD OP_EQUAL ``` 8) Input x(h(2) G), t = 1 ``` @@ -190,7 +196,7 @@ Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 h32 = 0000000000000000000000000000000000000000000000000000000000000001 expect = 136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785 - script = <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD <136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785> OP_EQUAL + script = <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD <136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785> OP_EQUAL ``` 9) Input x(h(7) G), t = 1 ``` @@ -198,7 +204,7 @@ Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 h32 = 0000000000000000000000000000000000000000000000000000000000000001 expect = 0e27b02714b3f2344f2bfa6d821654f2bd9f0ef497ec541b653b8dcb3a915faf - script = <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD <0e27b02714b3f2344f2bfa6d821654f2bd9f0ef497ec541b653b8dcb3a915faf> OP_EQUAL + script = <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD <0e27b02714b3f2344f2bfa6d821654f2bd9f0ef497ec541b653b8dcb3a915faf> OP_EQUAL ``` 10) Input x(G), t = 1 ``` @@ -206,7 +212,7 @@ Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 h32 = 4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a expect = c6713b2ac2495d1a879dc136abc06129a7bf355da486cd25f757e0a5f6f40f74 - script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> <4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a> OP_TWEAKADD OP_EQUAL + script = <4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_EQUAL ``` 11) Input x(G), t = h(2) ``` @@ -214,7 +220,7 @@ Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 h32 = dbc1b4c900ffe48d575b5da5c638040125f65db0fe3e24494b76ea986457d986 expect = 136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785 - script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD <136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785> OP_EQUAL + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD <136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785> OP_EQUAL ``` 12) Input x(G), t = h(7) (Note: differs from 9) ``` @@ -222,7 +228,7 @@ Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 h32 = ca358758f6d27e6cf45272937977a748fd88391db679ceda7dc7bf1f005ee879 expect = 00b152fb17d249541e3b2f51455269e02d76507ad7857aaa98e3c51ee5da5b1d - script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD <00b152fb17d249541e3b2f51455269e02d76507ad7857aaa98e3c51ee5da5b1d> OP_EQUAL + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD <00b152fb17d249541e3b2f51455269e02d76507ad7857aaa98e3c51ee5da5b1d> OP_EQUAL ``` ### Failing cases @@ -232,21 +238,21 @@ A) Scalar out of range (t = n) pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 h32 = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 expect = fail - script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_DROP OP_1 + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_DROP OP_1 ``` B) Invalid x (x = 0), t = 1 ``` pubkey32 = 0000000000000000000000000000000000000000000000000000000000000000 h32 = 0000000000000000000000000000000000000000000000000000000000000001 expect = fail - script = <0000000000000000000000000000000000000000000000000000000000000000> <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD OP_DROP OP_1 + script = <0000000000000000000000000000000000000000000000000000000000000001> <0000000000000000000000000000000000000000000000000000000000000000> OP_TWEAKADD OP_DROP OP_1 ``` C) Infinity result (x(G), t = n-1) ``` pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 h32 = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140 expect = fail - script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_DROP OP_1 + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_DROP OP_1 ``` ## Reference implementation notes diff --git a/bip-tweakadd/test-vectors/src/main.rs b/bip-tweakadd/test-vectors/src/main.rs index f19a5d1fd3..4ea1a99db8 100644 --- a/bip-tweakadd/test-vectors/src/main.rs +++ b/bip-tweakadd/test-vectors/src/main.rs @@ -31,12 +31,12 @@ fn case(name: &str, pubkey_hex: &str, t_hex: &str, check_res: Option<&str>) { assert_eq!(out_hex, check); } - let script = format!("<{pubkey_hex}> <{t_hex}> OP_TWEAKADD <{out_hex}> OP_EQUAL"); + let script = format!("<{t_hex}> <{pubkey_hex}> OP_TWEAKADD <{out_hex}> OP_EQUAL"); println!("{name}\n```\n pubkey32 = {pubkey_hex}\n h32 = {t_hex}\n expect = {out_hex}\n\n script = {script}\n```") } None => { - let script = format!("<{pubkey_hex}> <{t_hex}> OP_TWEAKADD OP_DROP OP_1"); + let script = format!("<{t_hex}> <{pubkey_hex}> OP_TWEAKADD OP_DROP OP_1"); println!("{name}\n```\n pubkey32 = {pubkey_hex}\n h32 = {t_hex}\n expect = fail\n script = {script}\n```") } } From 7602e08769c005a747b903507e45b298cc946e55 Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Sat, 23 Aug 2025 12:59:58 -0400 Subject: [PATCH 04/13] BIP Tweakadd: fix typo & add note on even-y tweaking --- bip-XXXX.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bip-XXXX.md b/bip-XXXX.md index 7a8f78c35c..850e8fc671 100644 --- a/bip-XXXX.md +++ b/bip-XXXX.md @@ -293,9 +293,10 @@ But it matters when a protocol tries to relate "a tweak applied at the base" (`x - If you want consistent algebraic relations across different ways of composing tweaks, **normalize scalars off-chain** before pushing them into script. - That is: replace every candidate tweak `s` with `adj(s)`, so that `adj(s)·G` has even Y. -- A simple library function can perform this parity check and adjustment using libsecp256k1; it does require a consensus modification or opcode. +- A simple library function can perform this parity check and adjustment using libsecp256k1 without a consensus modification or opcode. If the tweak is derived from inflexible state, such as a transaction hash or a signature, it may be infeasible to depend on commutativity of tweaking. +Protocols such as LN-Symmetry may simply grind the tx if even-y of tweak is required. From 14f28051a76d3bef8e75f7f410bdef83241b9362 Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Sat, 23 Aug 2025 13:42:25 -0400 Subject: [PATCH 05/13] BIP TweakAdd -- add mailing list discussion --- bip-XXXX.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bip-XXXX.md b/bip-XXXX.md index 850e8fc671..6f445ad349 100644 --- a/bip-XXXX.md +++ b/bip-XXXX.md @@ -306,8 +306,9 @@ This proposal extends the Taproot tweak mechanism (BIP340/341) into script, insp ## References +- [Bitcoin Dev Mailing List Discussion](https://groups.google.com/g/bitcoindev/c/-_geIB25zrg) - [CATT: Thoughts about an alternative covenant softfork proposal](https://delvingbitcoin.org/t/catt-thoughts-about-an-alternative-covenant-softfork-proposal/125) -- [Bitcoindev mailing list discussion](https://gnusha.org/pi/bitcoindev/e98d76f2-6f2c-9c3a-6a31-bccb34578c31@roose.io/) +- [Draft BIP: OP_TXHASH and OP_CHECKTXHASHVERIFY](https://gnusha.org/pi/bitcoindev/e98d76f2-6f2c-9c3a-6a31-bccb34578c31@roose.io/) - [Advent 8: Scriptless Scripts and Key Tweaks](https://rubin.io/bitcoin/2021/12/05/advent-8/) - [Re: [bitcoin-dev] Unlimited covenants, was Re: CHECKSIGFROMSTACK/{Verify} BIP for Bitcoin](https://gnusha.org/pi/bitcoindev/CAMZUoKnVLRLgL1rcq8DYHRjM--8VEUC5kjUbzbY5S860QSbk5w@mail.gmail.com/) - [Re: [bitcoin-dev] Unlimited covenants, was Re: CHECKSIGFROMSTACK/{Verify} BIP for Bitcoin](https://gnusha.org/pi/bitcoindev/CAMZUoKkAUodCT+2aQG71xwHYD8KXeTAdQq4NmXZ4GBe0pcD=9A@mail.gmail.com/) From 9415fbc4e4176d1510aea9a66eb52564fac3f52c Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Sat, 23 Aug 2025 14:35:15 -0400 Subject: [PATCH 06/13] BIP TweakAdd: Add Alpen and MATT mentions --- bip-XXXX.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bip-XXXX.md b/bip-XXXX.md index 6f445ad349..37267a2418 100644 --- a/bip-XXXX.md +++ b/bip-XXXX.md @@ -313,3 +313,5 @@ This proposal extends the Taproot tweak mechanism (BIP340/341) into script, insp - [Re: [bitcoin-dev] Unlimited covenants, was Re: CHECKSIGFROMSTACK/{Verify} BIP for Bitcoin](https://gnusha.org/pi/bitcoindev/CAMZUoKnVLRLgL1rcq8DYHRjM--8VEUC5kjUbzbY5S860QSbk5w@mail.gmail.com/) - [Re: [bitcoin-dev] Unlimited covenants, was Re: CHECKSIGFROMSTACK/{Verify} BIP for Bitcoin](https://gnusha.org/pi/bitcoindev/CAMZUoKkAUodCT+2aQG71xwHYD8KXeTAdQq4NmXZ4GBe0pcD=9A@mail.gmail.com/) - [ElementsProject: Tapscript opcodes documentation](https://github.com/ElementsProject/elements/blob/master/doc/tapscript_opcodes.md#new-opcodes-for-additional-functionality) +- [[bitcoin-dev] Merkleize All The Things](https://gnusha.org/pi/bitcoindev/CAMhCMoH9uZPeAE_2tWH6rf0RndqV+ypjbNzazpFwFnLUpPsZ7g@mail.gmail.com/) +- [Alpen Labs Technical-Whitepaper](https://github.com/alpenlabs/Technical-Whitepaper/tree/76d5279e62fe3f157ae94ffc0514ad2a95c6dbcf) \ No newline at end of file From e795d696939f073782dff8710f58e5a58f2126f2 Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Mon, 25 Aug 2025 20:32:04 -0400 Subject: [PATCH 07/13] BIP TweakAdd Formatting Edits --- bip-XXXX.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/bip-XXXX.md b/bip-XXXX.md index 37267a2418..6046240063 100644 --- a/bip-XXXX.md +++ b/bip-XXXX.md @@ -30,16 +30,16 @@ Bitcoin already leverages x-only key tweaking (for example, Taproot internal to ### Stack semantics -Input (top last): ``` -... \[h32] \[pubkey32] OP\_TWEAKADD -> ... \[pubkey32\_out] +... [h32] [pubkey32] OP_TWEAKADD -> ... [pubkey32_out] -```` +``` +Input: -- `h32`: 32-byte big-endian unsigned integer `t`. - `pubkey32`: 32-byte x-only public key (big-endian x coordinate). +- `h32`: 32-byte big-endian unsigned integer `t`. Output: @@ -314,4 +314,8 @@ This proposal extends the Taproot tweak mechanism (BIP340/341) into script, insp - [Re: [bitcoin-dev] Unlimited covenants, was Re: CHECKSIGFROMSTACK/{Verify} BIP for Bitcoin](https://gnusha.org/pi/bitcoindev/CAMZUoKkAUodCT+2aQG71xwHYD8KXeTAdQq4NmXZ4GBe0pcD=9A@mail.gmail.com/) - [ElementsProject: Tapscript opcodes documentation](https://github.com/ElementsProject/elements/blob/master/doc/tapscript_opcodes.md#new-opcodes-for-additional-functionality) - [[bitcoin-dev] Merkleize All The Things](https://gnusha.org/pi/bitcoindev/CAMhCMoH9uZPeAE_2tWH6rf0RndqV+ypjbNzazpFwFnLUpPsZ7g@mail.gmail.com/) -- [Alpen Labs Technical-Whitepaper](https://github.com/alpenlabs/Technical-Whitepaper/tree/76d5279e62fe3f157ae94ffc0514ad2a95c6dbcf) \ No newline at end of file +- [Alpen Labs Technical-Whitepaper](https://github.com/alpenlabs/Technical-Whitepaper/tree/76d5279e62fe3f157ae94ffc0514ad2a95c6dbcf) + +## Copyright + +This BIP is licensed under the [BSD-3-Clause License](https://opensource.org/licenses/BSD-3-Clause). \ No newline at end of file From 3e5044215f9c4657cc881932674ac52e7e3984c0 Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Wed, 27 Aug 2025 10:21:36 -0400 Subject: [PATCH 08/13] BIP TWEAKADD remove conventions section --- bip-XXXX.md | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/bip-XXXX.md b/bip-XXXX.md index 6046240063..86eab2b708 100644 --- a/bip-XXXX.md +++ b/bip-XXXX.md @@ -49,28 +49,23 @@ Output: Let `n` be the secp256k1 curve order. -1. Parse `h32` as big-endian integer `t`. If `t >= n`, fail. -2. Interpret `pubkey32` as an x-coordinate and attempt the BIP340 even-Y lift: +1. If `h32` or `pubkey32` are not 32 bytes, fail. +2. Parse `h32` as big-endian integer `t`. If `t >= n`, fail. +3. Interpret `pubkey32` as an x-coordinate and attempt the BIP340 even-Y lift: - If no curve point exists with that x, fail. - Otherwise, obtain `P` with even Y. -3. Compute `Q = P + t*G`. If `Q` is the point at infinity, fail. -4. Push `x(Q)` as a 32-byte big-endian value. +4. Compute `Q = P + t*G`. If `Q` is the point at infinity, fail. +5. Push `x(Q)` as a 32-byte big-endian value. Note: `t = 0` may fail if `pubkey32` is not valid. #### Script evaluation rules -0. If less than 2 stack elements, fail. -1. Pop `pubkey32` and then `h32` -2. If either length is not 32, fail. -3. Run `tweak_add` as above. -4. Push the 32-byte x-only result. - -### Conventions - -- X-only keys follow BIP340 conventions (even-Y). -- Scalars must be exactly 32 bytes, big-endian, and less than `n`. -- Non-32-byte inputs fail (consensus). Minimal push rules apply (policy). +1. If less than 2 stack elements, fail. +2. Pop `pubkey32` and then `h32` +3. If either length is not 32, fail. +4. Run `tweak_add` as above. +5. Push the 32-byte x-only result. ### Resource usage From c7540025cd2209701be53f19a6ae72f520eb4e75 Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Wed, 27 Aug 2025 10:22:17 -0400 Subject: [PATCH 09/13] BIP TWEAKADD formatting fix --- bip-XXXX.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bip-XXXX.md b/bip-XXXX.md index 86eab2b708..947849cdd0 100644 --- a/bip-XXXX.md +++ b/bip-XXXX.md @@ -118,7 +118,7 @@ def tweak_add(pubkey32: bytes, h32: bytes) -> bytes: if Q is None: # point at infinity raise ValueError return Q.x.to_bytes(32, 'big') -```` +``` ## Test vectors (Generated) From 4cb0456716edc22dbee411c7cd1fa8a4b0b4a353 Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Wed, 27 Aug 2025 10:23:02 -0400 Subject: [PATCH 10/13] BIP TWEAKADD Move Vectors to end --- bip-XXXX.md | 90 ++++++++++++++++++++++++++--------------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/bip-XXXX.md b/bip-XXXX.md index 947849cdd0..5c5d2c3476 100644 --- a/bip-XXXX.md +++ b/bip-XXXX.md @@ -121,6 +121,51 @@ def tweak_add(pubkey32: bytes, h32: bytes) -> bytes: ``` + +## Reference implementation notes + +* Reuse BIP340 lift/encode helpers from Taproot verification. +* Implement `t*G` via fixed-base multiplication, then combine with `P` using point addition. +* Serialize the result as 32-byte x-only. +* Charge EC op budget as 50, like `OP_CHECKSIGADD`. + + +## Protocol Design Note: Scalar Adjustment + +When working with x-only keys, it is important to remember that each 32-byte value encodes the equivalence class `{P, −P}`. +BIP340 defines the canonical lift as **the point with even Y**. As a result: + +- If an off-chain protocol describes an x-only key as "the point `s·G`," then in consensus terms the actual key is `adj(s)·G`, where: + +``` + +adj(s) = s if y(s·G) is even + = n − s if y(s·G) is odd + +``` + +- Consequently, `OP_TWEAKADD(x(s·G), t)` always computes: + +``` + +result = x(adj(s)·G + t·G) + +``` + +not simply `x(s·G + t·G)`. + +This distinction is invisible when signing or verifying against BIP340 keys, because both `s` and `n − s` yield the same x-only key. +But it matters when a protocol tries to relate "a tweak applied at the base" (`x(G), t = s`) to "a tweak applied at a derived key" (`x(s·G), t = 1`). In general those will differ unless the original point already had even Y. + + +- If you want consistent algebraic relations across different ways of composing tweaks, **normalize scalars off-chain** before pushing them into script. +- That is: replace every candidate tweak `s` with `adj(s)`, so that `adj(s)·G` has even Y. +- A simple library function can perform this parity check and adjustment using libsecp256k1 without a consensus modification or opcode. + +If the tweak is derived from inflexible state, such as a transaction hash or a signature, it may be infeasible to depend on commutativity of tweaking. +Protocols such as LN-Symmetry may simply grind the tx if even-y of tweak is required. + + ## Test vectors (Generated) @@ -250,51 +295,6 @@ C) Infinity result (x(G), t = n-1) script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_DROP OP_1 ``` -## Reference implementation notes - -* Reuse BIP340 lift/encode helpers from Taproot verification. -* Implement `t*G` via fixed-base multiplication, then combine with `P` using point addition. -* Serialize the result as 32-byte x-only. -* Charge EC op budget as 50, like `OP_CHECKSIGADD`. - - -## Protocol Design Note: Scalar Adjustment - -When working with x-only keys, it is important to remember that each 32-byte value encodes the equivalence class `{P, −P}`. -BIP340 defines the canonical lift as **the point with even Y**. As a result: - -- If an off-chain protocol describes an x-only key as "the point `s·G`," then in consensus terms the actual key is `adj(s)·G`, where: - -``` - -adj(s) = s if y(s·G) is even - = n − s if y(s·G) is odd - -``` - -- Consequently, `OP_TWEAKADD(x(s·G), t)` always computes: - -``` - -result = x(adj(s)·G + t·G) - -``` - -not simply `x(s·G + t·G)`. - -This distinction is invisible when signing or verifying against BIP340 keys, because both `s` and `n − s` yield the same x-only key. -But it matters when a protocol tries to relate "a tweak applied at the base" (`x(G), t = s`) to "a tweak applied at a derived key" (`x(s·G), t = 1`). In general those will differ unless the original point already had even Y. - - -- If you want consistent algebraic relations across different ways of composing tweaks, **normalize scalars off-chain** before pushing them into script. -- That is: replace every candidate tweak `s` with `adj(s)`, so that `adj(s)·G` has even Y. -- A simple library function can perform this parity check and adjustment using libsecp256k1 without a consensus modification or opcode. - -If the tweak is derived from inflexible state, such as a transaction hash or a signature, it may be infeasible to depend on commutativity of tweaking. -Protocols such as LN-Symmetry may simply grind the tx if even-y of tweak is required. - - - ## Acknowledgements This proposal extends the Taproot tweak mechanism (BIP340/341) into script, inspired by prior work on scriptless scripts and key-evolution constructions. There has been various discussion of OP_TWEAKADD over the years, including by Russell O'Connor and Steven Roose. From 4500b0ad25e3e61c58b26d563f1dcc232d21d7a8 Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Wed, 27 Aug 2025 10:27:14 -0400 Subject: [PATCH 11/13] BIP TweakAdd: Condense compatibility section --- bip-XXXX.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/bip-XXXX.md b/bip-XXXX.md index 5c5d2c3476..56c96e4e60 100644 --- a/bip-XXXX.md +++ b/bip-XXXX.md @@ -80,14 +80,13 @@ Note: `t = 0` may fail if `pubkey32` is not valid. - Push opcode rather than verification opcode for script compactness. - Argument order to permit tweak from witness onto fixed key without OP_SWAP. -## Backwards compatibility +## Compatibility -- Old nodes: treat unknown tapscript opcode as OP_SUCCESSx. -- This is a soft-fork change, tapscript-only. +This is a soft-fork change which is tapscript-only. Un-upgraded nodes will continue +to treat unknown tapscript opcode as OP_SUCCESSx. -## Future compatibility - -- A future OP_CAT or OP_TAPTREE opcode can prepare a tweak for a taproot output key correctly +A future upgrade, such as an OP_CAT or OP_TAPTREE opcode, can prepare a tweak for a +taproot output key correctly, if it is needed to create BIP-341 compatible outputs. ## Deployment From 5da11571b31bb2ae588d9add257c9e5343e12592 Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Sat, 28 Feb 2026 02:56:22 -0500 Subject: [PATCH 12/13] Update OP_TWEAKADD metadata Co-authored-by: Mark "Murch" Erhardt --- bip-XXXX.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bip-XXXX.md b/bip-XXXX.md index 56c96e4e60..167be86557 100644 --- a/bip-XXXX.md +++ b/bip-XXXX.md @@ -1,11 +1,11 @@ ``` -BIP: TBD +BIP: ? Layer: Consensus (soft fork) Title: OP_TWEAKADD - x-only key tweak addition -Author: Jeremy Rubin +Authors: Jeremy Rubin Status: Draft -Type: Standards Track -Created: 2025-08-22 +Type: Specification +Assigned: ? License: BSD-3-Clause ``` ## Abstract From 261453b9e569cf9c00e52ea71ea318f0c9666249 Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Tue, 3 Mar 2026 15:00:36 -0500 Subject: [PATCH 13/13] [OP_TWEAKADD] Consistently label tweak as `t` Co-authored-by: Mark "Murch" Erhardt --- bip-XXXX.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bip-XXXX.md b/bip-XXXX.md index 167be86557..5ffc25dc50 100644 --- a/bip-XXXX.md +++ b/bip-XXXX.md @@ -10,7 +10,7 @@ License: BSD-3-Clause ``` ## Abstract -This proposal defines a new tapscript opcode, `OP_TWEAKADD`, that takes an x-only public key and a 32-byte integer `h` on the stack and pushes the x-only public key corresponding to `P + h*G`, where `P` is the lifted point for the input x-coordinate and `G` is the secp256k1 generator. The operation mirrors the Taproot tweak used by BIP340 signers and enables simple, verifiable key modifications inside script without revealing private keys or relying on hash locks. +This proposal defines a new tapscript opcode, `OP_TWEAKADD`, that takes an x-only public key and a 32-byte integer `t` on the stack and pushes the x-only public key corresponding to `P + t*G`, where `P` is the lifted point for the input x-coordinate and `G` is the secp256k1 generator. The operation mirrors the Taproot tweak used by BIP340 signers and enables simple, verifiable key modifications inside script without revealing private keys or relying on hash locks. ## Motivation