diff --git a/README.mediawiki b/README.mediawiki index dfcb5a996e..d3a3cda842 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 - x-only key tweak addition +| Jeremy Rubin +| Specification +| Draft |} diff --git a/bip-0449.md b/bip-0449.md new file mode 100644 index 0000000000..c803808783 --- /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-0449/test-vectors/Cargo.toml b/bip-0449/test-vectors/Cargo.toml new file mode 100644 index 0000000000..cc27f5e697 --- /dev/null +++ b/bip-0449/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-0449/test-vectors/src/main.rs b/bip-0449/test-vectors/src/main.rs new file mode 100644 index 0000000000..4ea1a99db8 --- /dev/null +++ b/bip-0449/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!("<{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!("<{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```") + } + } +} + +/// 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, + ); +}