From 8ffa7379cfa4a29de0583bc28741393ccf246112 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 b349b181ca8d04cb7f9fd17b481e2cfa7cc21288 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 cd669f454adf5621287165c175c12e18d02579fe 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 858f801dd394539b7addbd1f8a311fbc090fa51c 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 5de43acce90e3d094fd93d6eb814bd9caf99a535 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 45d62e8ce3370afe94243e7dff05b7afbd743059 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 46910a504b6f5852bdd4f0346647d1fb0969edd5 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 466aac586e9d5f6bd8feeac6919eb184f42ef8ff 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 a6f271feac15e279c06d670faddf0202f096e058 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 ae05e47b562d69ca51f58c39be907e662dbe7b4f 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 76d8bbc712c159fad6279f12e2aee9ad44f124d0 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 b4a840e017d7ce3cb58a5652d39fdcd3a2ff8a8d Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Mon, 23 Mar 2026 13:12:05 -0400 Subject: [PATCH 12/13] [BIP-0449] Updates post assignemnt --- README.mediawiki | 7 + bip-0449.md | 458 ++++++++++++++++++ .../test-vectors/Cargo.toml | 0 .../test-vectors/src/main.rs | 0 bip-XXXX.md | 315 ------------ 5 files changed, 465 insertions(+), 315 deletions(-) create mode 100644 bip-0449.md rename {bip-tweakadd => bip-0449}/test-vectors/Cargo.toml (100%) rename {bip-tweakadd => bip-0449}/test-vectors/src/main.rs (100%) delete mode 100644 bip-XXXX.md diff --git a/README.mediawiki b/README.mediawiki index dfcb5a996e..dec79d8a63 100644 --- a/README.mediawiki +++ b/README.mediawiki @@ -1436,6 +1436,13 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority | Gregory Sanders, Antoine Poinsot, Steven Roose | Specification | Draft +|- +| [[bip-0449.md|449]] +| Consensus (soft fork) +| OP_TWEAKADD +| Jeremy Rubin +| Specification +| Draft |} diff --git a/bip-0449.md b/bip-0449.md new file mode 100644 index 0000000000..84302a76ea --- /dev/null +++ b/bip-0449.md @@ -0,0 +1,458 @@ +``` +BIP: 449 +Layer: Consensus (soft fork) +Title: OP_TWEAKADD - x-only key tweak addition +Authors: Jeremy Rubin +Status: Draft +Type: Specification +Assigned: 2026-03-05 +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 `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 + +Bitcoin already leverages x-only key tweaking when deriving Taproot output keys, but tapscript currently has no way to perform the same group operation inside script and continue using the derived key. Without such a primitive, protocols that depend on relations between keys must instead reveal preimages, commit to multiple full public keys, or perform additional signature checks at each step. + +Making key tweaking available in script enables several concrete constructions. These include tweak-reveal scripts built from `OP_TWEAKADD` together with proposed opcodes such as BIP 348 `OP_CHECKSIGFROMSTACK` and BIP 349 `OP_INTERNALKEY`, proof-of-signing-order constructions in which one signature commits to another signer acting later, delegation schemes in which one participant adjusts another participant's signing key, and target-tweak or key-reveal contracts that prove knowledge of a discrete logarithm by matching one tweaked key against another. + +In these constructions, the script commonly commits to the base key while the witness provides the scalar used to derive the spending key. `OP_TWEAKADD` therefore exposes only the elliptic-curve addition primitive and adopts an operand order that fits that pattern directly, while leaving hashing, Taproot-specific tweak construction, and higher-level protocol logic to script composition or future opcodes. + + +## 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 + + +``` + +... [tweak32] [pubkey32] OP_TWEAKADD -> ... [pubkey32_out] + +``` +Input: + +- `pubkey32`: 32-byte x-only public key (big-endian x coordinate). +- `tweak32`: 32-byte big-endian unsigned integer encoding scalar `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. If `tweak32` or `pubkey32` are not 32 bytes, fail. +2. Parse `tweak32` 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. +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 + +1. If less than 2 stack elements, fail. +2. Pop `pubkey32` and then `tweak32`. +3. If either length is not 32, fail. +4. Run `tweak_add` as above. +5. Push the 32-byte x-only result. + +### 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 + +### X-only encoding and failure conditions + +Using the BIP340 even-Y lift keeps the opcode aligned with the x-only key model already used by Taproot. Rejecting invalid lifts and the point at infinity ensures that `OP_TWEAKADD` never produces a value that cannot be interpreted as a usable x-only public key. + +The opcode also rejects scalars greater than or equal to the curve order instead of reducing them modulo the curve order. Modular reduction would permit distinct 32-byte inputs to encode the same effective tweak, introducing an unnecessary source of malleability. In constructions where the tweak is derived from witness data, for example ` OP_SHA256

OP_TWEAKADD`, this would allow different witness elements to produce the same derived key while changing transaction weight and fee characteristics. More generally, protocols should not be able to infer equality of tweak encodings from equality of derived keys when two distinct tweak values could be congruent modulo the curve order. Rejecting non-canonical scalars avoids these ambiguities. Applications that derive scalars from 32-byte hashes already need to handle the negligible-probability case in which the resulting value is not a valid secp256k1 scalar. + +### Push semantics + +`OP_TWEAKADD` pushes a derived key instead of directly verifying a signature. This keeps the opcode narrowly scoped and reusable. The derived key can feed into existing or proposed signature opcodes, equality checks, or other script logic, rather than forcing all uses through a single verification flow. + +### Raw tweak input + +The opcode accepts a raw 32-byte scalar instead of internally hashing the input or computing a Taproot-specific tweak. This keeps the consensus rule minimal. Constructions that require a hashed tweak can apply `OP_SHA256` before `OP_TWEAKADD`. Constructions that require a BIP341 tweak to be computed entirely on-chain would require additional general-purpose opcodes that expose tapleaf hashes or taptree commitments to script. + +### Argument order + +The chosen stack order puts the public key on top of the stack when `OP_TWEAKADD` executes. In the common pattern described above, the script pushes a fixed key and the witness supplies the scalar. This permits tweak-reveal, delegation, and proof-of-signing-order constructions to invoke `OP_TWEAKADD` without first inserting an `OP_SWAP`. + +## Compatibility + +This is a soft-fork change which is tapscript-only. Un-upgraded nodes will continue +to treat unknown tapscript opcode as OP_SUCCESSx. + +A future upgrade, such as `OP_CAT` or an opcode that exposes tapleaf hashes or taptree commitments to script, could prepare the 32-byte scalar required to derive BIP341-compatible Taproot output keys entirely on-chain. + +## Deployment + +TBD + +## Security considerations + +- Scalar range check prevents overflow, ambiguity, and alternate encodings of the same effective tweak. +- Infinity guard ensures valid outputs only. +- Scripts must control `t` derivation securely, which in many applications is trivial. +- No new witness malleability is introduced by alternate tweak encodings because tweaks must be exactly 32 bytes, must encode scalars less than the curve order, and x-only keys admit a unique even-Y lift. + +## Reference semantics (pseudocode) + +```python +SECP256K1_ORDER = n # 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 + +def tweak_add(pubkey32: bytes, tweak32: bytes) -> bytes: + if len(pubkey32) != 32 or len(tweak32) != 32: + raise ValueError + t = int.from_bytes(tweak32, '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') +``` + + + +## 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. + +## Protocol Design Note: Algebraic Limits + +`OP_TWEAKADD` implements only: + +```text +x(lift_x(X) + t*G) +``` + +If `X = x(G)`, then because the secp256k1 generator has even Y, this becomes: + +```text +x(G + t*G) = x((t + 1)*G) +``` + +This may suggest that repeated applications of `OP_TWEAKADD` can emulate an x-only scalar-multiplication opcode. In general this is not correct. After each application, the 32-byte x-only output is interpreted again using the BIP340 even-Y lift before the next tweak is added. Consequently: + +```text +OP_TWEAKADD(OP_TWEAKADD(x(G), t), n - 1) += x(adj(t + 1)*G - G) +``` + +which equals `x(t*G)` only when `(t + 1)*G` already has even Y. + +More generally, `OP_TWEAKADD` does not provide a generic `OP_ECMUL_XONLY` or `OP_ECADD` for hidden points. The opcode only adds multiples of the fixed generator `G`; it does not multiply an arbitrary point by a scalar, and x-only encodings do not retain the sign information needed to compose arbitrary point additions without additional data. As a result, `OP_TWEAKADD` is useful for expressing relations to a known base key or to generator-derived tweaks, but it does not subsume generic elliptic-curve arithmetic or signature verification primitives. For example, checking a Schnorr relation `s*G = R + e*P` requires scalar multiplication of an arbitrary public key `P` by `e` and point addition with `R`, neither of which can be implemented from `OP_TWEAKADD` alone without revealing additional information outside the x-only model. + + +## Illustrative Constructions + +The following sketches are non-normative examples. Some of them compose `OP_TWEAKADD` with other proposed opcodes, notably BIP 348 `OP_CHECKSIGFROMSTACK` and BIP 349 `OP_INTERNALKEY`. + +### Tweak-reveal scripts + +A tapscript may commit to a base key and accept a witness-provided scalar that must be revealed to derive the signing key. One such construction composes `OP_TWEAKADD` with BIP 348 `OP_CHECKSIGFROMSTACK` and BIP 349 `OP_INTERNALKEY`. When the construction requires the scalar to be hashed before use, the script can express that explicitly: + +```text +witness: +script: OP_SHA256 OP_INTERNALKEY OP_TWEAKADD OP_CHECKSIGFROMSTACK +``` + +Alternatively, with `OP_CHECKSIG`: + +```text +witness: +script: OP_SHA256 OP_INTERNALKEY OP_TWEAKADD OP_CHECKSIG +``` + +Applying `OP_SHA256` to the witness-provided scalar in these constructions prevents key cancellation. + +### Proof of signing order and transaction refinement + +`OP_TWEAKADD` can bind one signer's authorization to another signer's signature by tweaking key `A` with `SHA256(sig_B)`. The second signature then proves that `A` signed only after seeing `B`'s complete signature, so `B` can fix transaction details first and `A` can refine them later: + +```text +witness: +script: DUP TOALT CHECKSIGVERIFY FROMALT SHA256 OP_TWEAKADD OP_CHECKSIG +``` + +In this construction, `A` is bound to `B`'s complete signature. `B` may use any sighash flag combination, so `A` refines `B`'s signature rather than the reverse. + +For example, `B` may sign with `SIGHASH_SINGLE|SIGHASH_ANYONECANPAY` to commit only to one particular output paying `B`, while leaving input selection, fee payment, and change outputs unspecified. `A` may then sign the completed transaction with `SIGHASH_ALL`, selecting the concrete inputs, fee arrangement, and any additional outputs while remaining bound to that exact authorization from `B`. In this sense, `A` refines `B`'s partial authorization into authorization for one fully specified transaction. + +### Delegation + +A delegation protocol may allow `A` to delegate broad authority to key `B`. The predicate enforced by this script is that `B` produced a valid signature and that `A` authorized delegation to `B` by signing for the key derived from base key `A` and scalar `SHA256(B)`. The script verifies `B`'s signature first and then checks `A`'s delegation authorization. This verification order is chosen to avoid `OP_SWAP`; it does not imply that `B` must have signed before `A` authorized delegation. + +```text +witness: +script: DUP TOALT CHECKSIGVERIFY FROMALT SHA256 OP_TWEAKADD OP_CHECKSIG +``` + +If `A` uses a weak sighash mode such as `SIGHASH_NONE`, `A` need only authorize delegation to `B` and does not materially constrain the final transaction. `B` may then choose the concrete inputs, outputs, and fees, and provide the transaction signature that refines `A`'s permissive authorization into authorization for one fully specified transaction. + +Unlike the signing-order construction above, this construction does not require `OP_CHECKSIGFROMSTACK`, because `A` need only be bound to `B`'s public key rather than to `B`'s eventual signature bytes. + +### Target-tweak and key-reveal contracts + +In its simplest form, `OP_TWEAKADD` can prove knowledge of a scalar `t` such that `tG + k1G = k2G`: + +```text +witness: +script: OP_TWEAKADD OP_EQUAL +``` + +If the construction requires the scalar to be hashed first, the script can express that directly: + +```text +witness: +script: OP_SHA256 OP_TWEAKADD OP_EQUAL +``` + +If `k2G` is used as a Taproot output key, this construction can force disclosure of the scalar needed to account for the corresponding Taproot tweak. + +A key-reveal contract is a special case of the target-tweak construction. Given `T = tG`, the script can require disclosure of `t` by checking that `G + tG = T + G`: + +```text +witness: +script: OP_TWEAKADD OP_EQUAL +``` + +### Merkleized commitments + +`OP_TWEAKADD` can also be used to commit to the root of a Merkleized data structure by treating the root hash as a tweak scalar. Let `K_root = tweak(P, H(root))`, where `root` is the Merkle root of some application state or set of valid transitions and `H(root)` is interpreted as a secp256k1 scalar when valid. Revealing the root can then be checked with: + +```text +witness: +script: OP_SHA256

OP_TWEAKADD OP_EQUAL +``` + +This permits a script to commit to a large data structure using one derived key while deferring disclosure of the underlying tree to the witness. If the script also has a way to compute parent hashes from child hashes, for example `OP_CAT OP_SHA256`, then a Merkle branch can be verified by iteratively recomputing `root` from a revealed leaf and its sibling path, and then checking the resulting root against the committed key above. In that setting, `OP_TWEAKADD` provides the bridge from a Merkle root to a key that can be consumed by the rest of the script, allowing Merkleized state or transition commitments in the style of constructions such as MATT. + +Even without arbitrary concatenation opcodes, `OP_TWEAKADD` can be used to build a Merkle-like authenticated tree over 32-byte node commitments. Let `B_leaf = x(2G)` and `B_node = x(3G)` be distinct fixed x-only public keys, and define: + +```text +Leaf(x) = tweak(B_leaf, SHA256(x)) +Node(L,R) = tweak(tweak(B_node, HASH256(L)), SHA256(HASH256(R))) +``` + +Here the point input is always either a fixed valid x-only key or the output of a prior `OP_TWEAKADD`, so no hash-to-point grinding is required. The different hash constructions separate the left-child and right-child roles, while the distinct base keys separate leaf commitments from internal-node commitments. + +One parent reduction can be expressed directly in script: + +```text +witness: +script: OP_TOALTSTACK OP_HASH256 OP_TWEAKADD + OP_FROMALTSTACK OP_HASH256 OP_SHA256 OP_SWAP OP_TWEAKADD +``` + +A branch proof starts from `Leaf(x)` for the revealed leaf value `x`, then iteratively combines it with each sibling commitment using the appropriate left or right parent rule until the committed root key is reconstructed. + +This construction should be understood as an ordered binary commitment tree rather than as a general Merkle compression opcode. The recursive use of `SHA256`, `HASH256`, and `SHA256(HASH256(.))` separates roles, but it does not by itself define a canonical tree shape. A protocol using this pattern should therefore also fix the tree arity, proof length, and any empty-subtree or append rules so that the same application state cannot be represented by multiple tree layouts. + +For comparison, BIP 442 `OP_PAIRCOMMIT` is purpose-built for pair commitments and yields simpler and cheaper Merkleized constructions in practice. The construction above is therefore best understood as a potentially useful tree-commitment technique if `OP_TWEAKADD` were deployed on its own, rather than as a primary reason to add `OP_TWEAKADD` when a dedicated pair-commitment opcode is available. + +## Test vectors (Generated) + + +Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 + + +### Passing cases + +1) Identity tweak (t = 0) +``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + tweak32 = 0000000000000000000000000000000000000000000000000000000000000000 + expect = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + + script = <0000000000000000000000000000000000000000000000000000000000000000> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_EQUAL +``` +2) Increment by 1 +``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + tweak32 = 0000000000000000000000000000000000000000000000000000000000000001 + expect = c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 + + script = <0000000000000000000000000000000000000000000000000000000000000001> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_EQUAL +``` +3) Increment by 2 +``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + tweak32 = 0000000000000000000000000000000000000000000000000000000000000002 + expect = f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9 + + script = <0000000000000000000000000000000000000000000000000000000000000002> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_EQUAL +``` +4) Increment by 5 +``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + tweak32 = 0000000000000000000000000000000000000000000000000000000000000005 + expect = fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556 + + script = <0000000000000000000000000000000000000000000000000000000000000005> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_EQUAL +``` +5) Input x(2G), t = 3 +``` + pubkey32 = c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 + tweak32 = 0000000000000000000000000000000000000000000000000000000000000003 + expect = 2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4 + + script = <0000000000000000000000000000000000000000000000000000000000000003> OP_TWEAKADD <2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4> OP_EQUAL +``` +6) Input x(7G), t = 9 +``` + pubkey32 = 5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc + tweak32 = 0000000000000000000000000000000000000000000000000000000000000009 + expect = e60fce93b59e9ec53011aabc21c23e97b2a31369b87a5ae9c44ee89e2a6dec0a + + script = <0000000000000000000000000000000000000000000000000000000000000009> <5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc> OP_TWEAKADD OP_EQUAL +``` +7) Input x(h(1) G), t = 1 +``` + pubkey32 = d415b187c6e7ce9da46ac888d20df20737d6f16a41639e68ea055311e1535dd9 + tweak32 = 0000000000000000000000000000000000000000000000000000000000000001 + expect = c6713b2ac2495d1a879dc136abc06129a7bf355da486cd25f757e0a5f6f40f74 + + script = <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD OP_EQUAL +``` +8) Input x(h(2) G), t = 1 +``` + pubkey32 = d27cd27dbff481bc6fc4aa39dd19405eb6010237784ecba13bab130a4a62df5d + tweak32 = 0000000000000000000000000000000000000000000000000000000000000001 + expect = 136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785 + + script = <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD <136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785> OP_EQUAL +``` +9) Input x(h(7) G), t = 1 +``` + pubkey32 = ddc399701a78edd5ea56429b2b7b6cd11f7d1e4015e7830b4f5e07eb25058768 + tweak32 = 0000000000000000000000000000000000000000000000000000000000000001 + expect = 0e27b02714b3f2344f2bfa6d821654f2bd9f0ef497ec541b653b8dcb3a915faf + + script = <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD <0e27b02714b3f2344f2bfa6d821654f2bd9f0ef497ec541b653b8dcb3a915faf> OP_EQUAL +``` +10) Input x(G), t = 1 +``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + tweak32 = 4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a + expect = c6713b2ac2495d1a879dc136abc06129a7bf355da486cd25f757e0a5f6f40f74 + + script = <4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_EQUAL +``` +11) Input x(G), t = h(2) +``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + tweak32 = dbc1b4c900ffe48d575b5da5c638040125f65db0fe3e24494b76ea986457d986 + expect = 136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785 + + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD <136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785> OP_EQUAL +``` +12) Input x(G), t = h(7) (Note: differs from 9) +``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + tweak32 = ca358758f6d27e6cf45272937977a748fd88391db679ceda7dc7bf1f005ee879 + expect = 00b152fb17d249541e3b2f51455269e02d76507ad7857aaa98e3c51ee5da5b1d + + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD <00b152fb17d249541e3b2f51455269e02d76507ad7857aaa98e3c51ee5da5b1d> OP_EQUAL +``` + +### Failing cases + +A) Scalar out of range (t = n) +``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + tweak32 = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 + expect = fail + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_DROP OP_1 +``` +B) Invalid x (x = 0), t = 1 +``` + pubkey32 = 0000000000000000000000000000000000000000000000000000000000000000 + tweak32 = 0000000000000000000000000000000000000000000000000000000000000001 + expect = fail + script = <0000000000000000000000000000000000000000000000000000000000000001> <0000000000000000000000000000000000000000000000000000000000000000> OP_TWEAKADD OP_DROP OP_1 +``` +C) Infinity result (x(G), t = n-1) +``` + pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 + tweak32 = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140 + expect = fail + script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_DROP OP_1 +``` + +## 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 + +- [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) +- [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/) +- [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) + +## Copyright + +This BIP is licensed under the [BSD-3-Clause License](https://opensource.org/licenses/BSD-3-Clause). diff --git a/bip-tweakadd/test-vectors/Cargo.toml b/bip-0449/test-vectors/Cargo.toml similarity index 100% rename from bip-tweakadd/test-vectors/Cargo.toml rename to bip-0449/test-vectors/Cargo.toml diff --git a/bip-tweakadd/test-vectors/src/main.rs b/bip-0449/test-vectors/src/main.rs similarity index 100% rename from bip-tweakadd/test-vectors/src/main.rs rename to bip-0449/test-vectors/src/main.rs diff --git a/bip-XXXX.md b/bip-XXXX.md deleted file mode 100644 index 56c96e4e60..0000000000 --- a/bip-XXXX.md +++ /dev/null @@ -1,315 +0,0 @@ -``` -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 - - -``` - -... [h32] [pubkey32] OP_TWEAKADD -> ... [pubkey32_out] - -``` -Input: - -- `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. 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. -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 - -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 - -- 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. -- Argument order to permit tweak from witness onto fixed key without OP_SWAP. - -## Compatibility - -This is a soft-fork change which is tapscript-only. Un-upgraded nodes will continue -to treat unknown tapscript opcode as OP_SUCCESSx. - -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 - -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') -``` - - - -## 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) - - -Curve order n = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 - - -### Passing cases - -1) Identity tweak (t = 0) -``` - pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 - h32 = 0000000000000000000000000000000000000000000000000000000000000000 - expect = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 - - script = <0000000000000000000000000000000000000000000000000000000000000000> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_EQUAL -``` -2) Increment by 1 -``` - pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 - h32 = 0000000000000000000000000000000000000000000000000000000000000001 - expect = c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 - - script = <0000000000000000000000000000000000000000000000000000000000000001> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_EQUAL -``` -3) Increment by 2 -``` - pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 - h32 = 0000000000000000000000000000000000000000000000000000000000000002 - expect = f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9 - - script = <0000000000000000000000000000000000000000000000000000000000000002> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_EQUAL -``` -4) Increment by 5 -``` - pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 - h32 = 0000000000000000000000000000000000000000000000000000000000000005 - expect = fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556 - - script = <0000000000000000000000000000000000000000000000000000000000000005> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_EQUAL -``` -5) Input x(2G), t = 3 -``` - pubkey32 = c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5 - h32 = 0000000000000000000000000000000000000000000000000000000000000003 - expect = 2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4 - - script = <0000000000000000000000000000000000000000000000000000000000000003> OP_TWEAKADD <2f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4> OP_EQUAL -``` -6) Input x(7G), t = 9 -``` - pubkey32 = 5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc - h32 = 0000000000000000000000000000000000000000000000000000000000000009 - expect = e60fce93b59e9ec53011aabc21c23e97b2a31369b87a5ae9c44ee89e2a6dec0a - - script = <0000000000000000000000000000000000000000000000000000000000000009> <5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc> OP_TWEAKADD OP_EQUAL -``` -7) Input x(h(1) G), t = 1 -``` - pubkey32 = d415b187c6e7ce9da46ac888d20df20737d6f16a41639e68ea055311e1535dd9 - h32 = 0000000000000000000000000000000000000000000000000000000000000001 - expect = c6713b2ac2495d1a879dc136abc06129a7bf355da486cd25f757e0a5f6f40f74 - - script = <0000000000000000000000000000000000000000000000000000000000000001> OP_TWEAKADD OP_EQUAL -``` -8) Input x(h(2) G), t = 1 -``` - pubkey32 = d27cd27dbff481bc6fc4aa39dd19405eb6010237784ecba13bab130a4a62df5d - h32 = 0000000000000000000000000000000000000000000000000000000000000001 - expect = 136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785 - - 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 -``` -10) Input x(G), t = 1 -``` - pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 - h32 = 4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a - expect = c6713b2ac2495d1a879dc136abc06129a7bf355da486cd25f757e0a5f6f40f74 - - script = <4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a> <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_EQUAL -``` -11) Input x(G), t = h(2) -``` - pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 - h32 = dbc1b4c900ffe48d575b5da5c638040125f65db0fe3e24494b76ea986457d986 - expect = 136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785 - - script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD <136f23e6c2efcaa13b37f0c22cd6cfb0d4e6e9eddccefe17e747f5cf440bb785> OP_EQUAL -``` -12) Input x(G), t = h(7) (Note: differs from 9) -``` - pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 - h32 = ca358758f6d27e6cf45272937977a748fd88391db679ceda7dc7bf1f005ee879 - expect = 00b152fb17d249541e3b2f51455269e02d76507ad7857aaa98e3c51ee5da5b1d - - script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD <00b152fb17d249541e3b2f51455269e02d76507ad7857aaa98e3c51ee5da5b1d> OP_EQUAL -``` - -### Failing cases - -A) Scalar out of range (t = n) -``` - pubkey32 = 79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798 - h32 = fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 - expect = fail - script = <79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798> OP_TWEAKADD OP_DROP OP_1 -``` -B) Invalid x (x = 0), t = 1 -``` - pubkey32 = 0000000000000000000000000000000000000000000000000000000000000000 - h32 = 0000000000000000000000000000000000000000000000000000000000000001 - expect = fail - 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 -``` - -## 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 - -- [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) -- [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/) -- [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) - -## 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 8cf8e3b103fd9a019f635fc64d12a3e1de696730 Mon Sep 17 00:00:00 2001 From: Jeremy Rubin Date: Mon, 23 Mar 2026 15:43:18 -0400 Subject: [PATCH 13/13] [BIP-0449] Normalize Metadata --- README.mediawiki | 2 +- bip-0449.md | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.mediawiki b/README.mediawiki index dec79d8a63..d3a3cda842 100644 --- a/README.mediawiki +++ b/README.mediawiki @@ -1439,7 +1439,7 @@ users (see also: [https://en.bitcoin.it/wiki/Economic_majority economic majority |- | [[bip-0449.md|449]] | Consensus (soft fork) -| OP_TWEAKADD +| OP_TWEAKADD - x-only key tweak addition | Jeremy Rubin | Specification | Draft diff --git a/bip-0449.md b/bip-0449.md index 84302a76ea..c803808783 100644 --- a/bip-0449.md +++ b/bip-0449.md @@ -1,12 +1,12 @@ ``` -BIP: 449 -Layer: Consensus (soft fork) -Title: OP_TWEAKADD - x-only key tweak addition -Authors: Jeremy Rubin -Status: Draft -Type: Specification -Assigned: 2026-03-05 -License: BSD-3-Clause + BIP: 449 + Layer: Consensus (soft fork) + Title: OP_TWEAKADD - x-only key tweak addition + Authors: Jeremy Rubin + Status: Draft + Type: Specification + Assigned: 2026-03-05 + License: BSD-3-Clause ``` ## Abstract