1
0
mirror of https://github.com/bitcoin/bips.git synced 2026-07-20 18:05:59 +00:00

BIP-0441: update wordspan costs and OP_RIGHT

- Apply BIP-0440 wordspan notation to numeric and bitvector costs.

- Define OP_RIGHT as rightmost-byte extraction with bounded copying cost.

- Clarify OP_UPSHIFT, CLTV/CSV, and final success-check costs.
This commit is contained in:
julian
2026-06-15 21:26:51 +02:00
parent 503939b53d
commit e840999d2d

View File

@@ -9,7 +9,7 @@
Assigned: 2026-03-25 Assigned: 2026-03-25
License: BSD-3-Clause License: BSD-3-Clause
Discussion: https://groups.google.com/g/bitcoindev/c/GisTcPb8Jco/m/8znWcWwKAQAJ Discussion: https://groups.google.com/g/bitcoindev/c/GisTcPb8Jco/m/8znWcWwKAQAJ
Version: 0.2.1 Version: 0.2.2
Requires: 440 Requires: 440
</pre> </pre>
@@ -154,6 +154,13 @@ stack as arbitrary-length little-endian values (instead of CScriptNum):
# OP_IFDUP # OP_IFDUP
# OP_CHECKSIGADD # OP_CHECKSIGADD
For OP_CHECKLOCKTIMEVERIFY and OP_CHECKSEQUENCEVERIFY, the operand is decoded
and costed as an arbitrary-length unsigned integer. However, the decoded
value MUST be less than 2<sup>32</sup>, because it is compared against the
32-bit nLockTime or nSequence transaction field. OP_CHECKSEQUENCEVERIFY
performs this range check before disable-flag handling or BIP68 masking, so
any non-zero bits above bit 31 cause failure.
These opcodes are redefined in 0xC2 Tapscript to write numbers to the stack as These opcodes are redefined in 0xC2 Tapscript to write numbers to the stack as
minimal-length little-endian values (instead of CScriptNum): minimal-length little-endian values (instead of CScriptNum):
@@ -175,6 +182,11 @@ Now:
``4. (ii) If the execution results in anything but exactly one element on the ``4. (ii) If the execution results in anything but exactly one element on the
stack which contains one or more non-zero bytes, fail.`` stack which contains one or more non-zero bytes, fail.``
This final success check consumes varops budget as <code>wordspan(A) * 2</code>
(COMPARINGZERO), where <code>A</code> is the remaining stack element. If this
cost exceeds the remaining transaction varops budget, fail before performing
the non-zero-byte check.
===Enabled Opcodes=== ===Enabled Opcodes===
Fifteen opcodes that were removed in v0.3.1 are re-enabled in 0xC2 Tapscript. Fifteen opcodes that were removed in v0.3.1 are re-enabled in 0xC2 Tapscript.
@@ -187,6 +199,15 @@ stack.
See [[bip-0440.mediawiki|BIP440]] for the meaning of the See [[bip-0440.mediawiki|BIP440]] for the meaning of the
annotations in the varops cost field. annotations in the varops cost field.
====Byte Lengths and Word Spans====
This BIP uses the <code>length(...)</code> and <code>wordspan(...)</code>
convention from [[bip-0440.mediawiki|BIP440]]: <code>length(X)</code> is the
script-visible byte length of stack element <code>X</code>, while
<code>wordspan(X)</code> is that length rounded up to the 64-bit word span used
for numeric and bit-vector work. Some formulas intentionally mix the two when
an opcode performs both word-rounded interpretation and exact byte movement.
====Splice Opcodes==== ====Splice Opcodes====
{| {|
@@ -218,7 +239,7 @@ annotations in the varops cost field.
# Remove BEGIN bytes from the front of A (all bytes if BEGIN is greater than length of A). # Remove BEGIN bytes from the front of A (all bytes if BEGIN is greater than length of A).
# If length(A) is greater than value(LEN), truncate A to length value(LEN). # If length(A) is greater than value(LEN), truncate A to length value(LEN).
# Push A onto the stack. # Push A onto the stack.
|(length(LEN) + length(BEGIN)) * 2 + MIN(Value of LEN, MAX(length(A) - Value of BEGIN, 0)) * 3 |(wordspan(LEN) + wordspan(BEGIN)) * 2 + MIN(Value of LEN, MAX(length(A) - Value of BEGIN, 0)) * 3
|LENGTHCONV + COPYING |LENGTHCONV + COPYING
|- |-
|OP_LEFT |OP_LEFT
@@ -229,18 +250,20 @@ annotations in the varops cost field.
# Pop operands off the stack. # Pop operands off the stack.
# If length(A) is greater than value(OFFSET), truncate A to length value(OFFSET). # If length(A) is greater than value(OFFSET), truncate A to length value(OFFSET).
# Push A onto the stack. # Push A onto the stack.
|length(OFFSET) * 2 |wordspan(OFFSET) * 2
|LENGTHCONV |LENGTHCONV
|- |-
|OP_RIGHT |OP_RIGHT
|129 |129
|<nowiki>[A OFFSET]</nowiki> |<nowiki>[A OFFSET]</nowiki>
|Extract the right bytes of A, from OFFSET onwards |Extract the rightmost OFFSET bytes of A
| |
# Pop operands off the stack. # Pop operands off the stack.
# If value(OFFSET) is less than length(A), copy value(OFFSET) bytes from offset value(OFFSET) to offset 0 in A, and truncate A to length(A) - value(OFFSET). Otherwise truncate A to length 0. # Convert OFFSET to a bounded length value.
# If value(OFFSET) is less than length(A), remove length(A) - value(OFFSET) bytes from the front of A.
# Otherwise leave A unchanged.
# Push A onto the stack. # Push A onto the stack.
|length(OFFSET) * 2 + value of OFFSET * 3 |wordspan(OFFSET) * 2 + MIN(Value of OFFSET, length(A)) * 3
|LENGTHCONV + COPYING |LENGTHCONV + COPYING
|} |}
@@ -252,7 +275,8 @@ OP_SUBSTR may have to copy LEN bytes, but also needs to read its two numeric
operands. LEN is limited to the length of the operand minus BEGIN. operands. LEN is limited to the length of the operand minus BEGIN.
OP_LEFT only needs to read its OFFSET operand (truncation is free), whereas OP_LEFT only needs to read its OFFSET operand (truncation is free), whereas
OP_RIGHT must copy the bytes, which depends on the OFFSET value. OP_RIGHT must copy the rightmost bytes, which depends on the bounded OFFSET
value.
====Bit Operation Opcodes==== ====Bit Operation Opcodes====
@@ -273,7 +297,7 @@ OP_RIGHT must copy the bytes, which depends on the OFFSET value.
# Pop operands off the stack. # Pop operands off the stack.
# For each byte in A, replace it with that byte bitwise XOR 0xFF (i.e. invert the bits) # For each byte in A, replace it with that byte bitwise XOR 0xFF (i.e. invert the bits)
# Push A onto the stack. # Push A onto the stack.
|length(A) * 4 |wordspan(A) * 4
|OTHER |OTHER
|- |-
|OP_AND |OP_AND
@@ -285,7 +309,7 @@ OP_RIGHT must copy the bytes, which depends on the OFFSET value.
# If B is longer than A, swap B and A. # If B is longer than A, swap B and A.
# For each byte in A (the longer operand): bitwise AND it with the equivalent byte in B (or 0 if past end of B) # For each byte in A (the longer operand): bitwise AND it with the equivalent byte in B (or 0 if past end of B)
# Push A onto the stack. # Push A onto the stack.
|(length(A) + length(B)) * 2 |(wordspan(A) + wordspan(B)) * 2
|OTHER + ZEROING |OTHER + ZEROING
|- |-
|OP_OR |OP_OR
@@ -297,7 +321,7 @@ OP_RIGHT must copy the bytes, which depends on the OFFSET value.
# If B is longer than A, swap B and A. # If B is longer than A, swap B and A.
# For each byte in B (the shorter operand): bitwise OR it into the equivalent byte in A (altering A). # For each byte in B (the shorter operand): bitwise OR it into the equivalent byte in A (altering A).
# Push A onto the stack. # Push A onto the stack.
|MIN(length(A), length(B)) * 4 |MIN(wordspan(A), wordspan(B)) * 4
|OTHER |OTHER
|- |-
|OP_XOR |OP_XOR
@@ -309,7 +333,7 @@ OP_RIGHT must copy the bytes, which depends on the OFFSET value.
# If B is longer than A, swap B and A. # If B is longer than A, swap B and A.
# For each byte in B (the shorter operand): exclusive OR it into the equivalent byte in A (altering A). # For each byte in B (the shorter operand): exclusive OR it into the equivalent byte in A (altering A).
# Push A onto the stack. # Push A onto the stack.
|MIN(length(A), length(B)) * 4 |MIN(wordspan(A), wordspan(B)) * 4
|OTHER |OTHER
|} |}
@@ -347,8 +371,8 @@ OP_DOWNSHIFT (née OP_RSHIFT).
# If value(BITS) % 8 == 0: simply prepend value(BITS) / 8 zeroes to A. # If value(BITS) % 8 == 0: simply prepend value(BITS) / 8 zeroes to A.
# Otherwise: prepend (value(BITS) / 8) + 1 zeroes to A, then shift A *down* (8 - (value(BITS) % 8)) bits. # Otherwise: prepend (value(BITS) / 8) + 1 zeroes to A, then shift A *down* (8 - (value(BITS) % 8)) bits.
# Push A onto the stack. # Push A onto the stack.
|length(BITS) * 2 + (Value of BITS) / 8 * 2 + length(A) * 3. If BITS % 8 != 0, add length(A) * 4 |wordspan(BITS) * 2 + (Value of BITS) / 8 * 2 + length(A) * 3. If BITS % 8 != 0, add wordspan(length(A) + (Value of BITS) / 8) * 4
|LENGTHCONV + ZEROING + COPYING. If BITS % 8 != 0, + OTHER. |LENGTHCONV + ZEROING + COPYING. If BITS % 8 != 0, + OTHER over the shifted result length.
|- |-
|OP_DOWNSHIFT |OP_DOWNSHIFT
|153 |153
@@ -360,7 +384,7 @@ OP_DOWNSHIFT (née OP_RSHIFT).
## Copy each bit in A from BITOFF + value(BITS) to BITOFF. ## Copy each bit in A from BITOFF + value(BITS) to BITOFF.
# Truncate A to remove value(BITS) / 8 bytes from the end (or all bytes, if value(BITS) / 8 > length(A)). # Truncate A to remove value(BITS) / 8 bytes from the end (or all bytes, if value(BITS) / 8 > length(A)).
# Push A onto the stack. # Push A onto the stack.
|length(BITS) * 2 + MAX((length(A) - (Value of BITS) / 8), 0) * 3 |wordspan(BITS) * 2 + MAX((length(A) - (Value of BITS) / 8), 0) * 3
|LENGTHCONV + COPYING |LENGTHCONV + COPYING
|} |}
@@ -401,7 +425,7 @@ routine as OP_DOWNSHIFT.
# If the final byte overflows, append a single 1 byte. # If the final byte overflows, append a single 1 byte.
# Otherwise, truncate A at the last non-zero byte. # Otherwise, truncate A at the last non-zero byte.
# Push A onto the stack. # Push A onto the stack.
|length(A) * 7 |wordspan(A) * 7
|OTHER + COPYING |OTHER + COPYING
|- |-
|OP_2DIV |OP_2DIV
@@ -413,7 +437,7 @@ routine as OP_DOWNSHIFT.
# Shift each byte in A 1 bit to the right (decreasing values, equivalent to C's >> operator), taking the next bytes bottom bit as the value of the top bit, and tracking the last non-zero value. # Shift each byte in A 1 bit to the right (decreasing values, equivalent to C's >> operator), taking the next bytes bottom bit as the value of the top bit, and tracking the last non-zero value.
# Truncate A at the last non-zero byte. # Truncate A at the last non-zero byte.
# Push A onto the stack. # Push A onto the stack.
|length(A) * 4 |wordspan(A) * 4
|OTHER |OTHER
|- |-
|OP_MUL |OP_MUL
@@ -427,7 +451,7 @@ routine as OP_DOWNSHIFT.
# For each word in A, multiply it by B and add it into the vector R, offset by the word offset in A. # For each word in A, multiply it by B and add it into the vector R, offset by the word offset in A.
# Truncate R at the last non-zero byte. # Truncate R at the last non-zero byte.
# Push R onto the stack. # Push R onto the stack.
|(length(A) + length(B)) * 3 + (length(A) + 7) / 8 * length(B) * 27 (BEWARE OVERFLOW) |(length(A) + length(B)) * 3 + wordspan(A) / 8 * wordspan(B) * 27 (BEWARE OVERFLOW)
|See Appendix |See Appendix
|- |-
|OP_DIV |OP_DIV
@@ -441,7 +465,7 @@ routine as OP_DOWNSHIFT.
# Perform division as per Knuth's The Art of Computer Programming v2 page 272, Algorithm D "Division of non-negative integers". # Perform division as per Knuth's The Art of Computer Programming v2 page 272, Algorithm D "Division of non-negative integers".
# Trim trailing zeroes off the quotient. # Trim trailing zeroes off the quotient.
# Push the quotient onto the stack. # Push the quotient onto the stack.
|length(A) * 18 + length(B) * 4 + length(A)^2 * 2 / 3 (BEWARE OVERFLOW) |wordspan(A) * 18 + wordspan(B) * 4 + wordspan(A)^2 * 2 / 3 (BEWARE OVERFLOW)
|See Appendix |See Appendix
|- |-
|OP_MOD |OP_MOD
@@ -455,7 +479,7 @@ routine as OP_DOWNSHIFT.
# Perform division as per Knuth's The Art of Computer Programming v2 page 272, Algorithm D "Division of non-negative integers". # Perform division as per Knuth's The Art of Computer Programming v2 page 272, Algorithm D "Division of non-negative integers".
# Trim trailing zeroes off the remainder. # Trim trailing zeroes off the remainder.
# Push the remainder onto the stack. # Push the remainder onto the stack.
|length(A) * 18 + length(B) * 4 + length(A)^2 * 2 / 3 (BEWARE OVERFLOW) |wordspan(A) * 18 + wordspan(B) * 4 + wordspan(A)^2 * 2 / 3 (BEWARE OVERFLOW)
|See Appendix |See Appendix
|} |}
@@ -494,7 +518,7 @@ The opcodes OP_ADD, OP_SUB, OP_1ADD and OP_1SUB are redefined in 0xC2 Tapscript
# If there was final overflow, append a 1 byte to A. # If there was final overflow, append a 1 byte to A.
# Option 2: If there was no final overflow, remember last non-zero byte written into A, and truncate A after that point. # Option 2: If there was no final overflow, remember last non-zero byte written into A, and truncate A after that point.
# Either Option 1 or Option 2 MUST be implemented. # Either Option 1 or Option 2 MUST be implemented.
|MAX(length(A), length(B)) * 9 |MAX(wordspan(A), wordspan(B)) * 9
|ARITH + COPYING |ARITH + COPYING
|- |-
|OP_1ADD |OP_1ADD
@@ -504,7 +528,7 @@ The opcodes OP_ADD, OP_SUB, OP_1ADD and OP_1SUB are redefined in 0xC2 Tapscript
| |
# Pop operands off the stack. # Pop operands off the stack.
# Let B = 1, and continue as OP_ADD. # Let B = 1, and continue as OP_ADD.
|MAX(1, length(A)) * 9 |MAX(wordspan(1), wordspan(A)) * 9
|ARITH + COPYING |ARITH + COPYING
|- |-
|OP_SUB |OP_SUB
@@ -516,7 +540,7 @@ The opcodes OP_ADD, OP_SUB, OP_1ADD and OP_1SUB are redefined in 0xC2 Tapscript
# For each byte in B, subtract it and previous underflow from the equivalent byte in A, remembering next underflow. # For each byte in B, subtract it and previous underflow from the equivalent byte in A, remembering next underflow.
# If there was final underflow, fail validation. # If there was final underflow, fail validation.
# Remember last non-zero byte written into A, and truncate A after that point. # Remember last non-zero byte written into A, and truncate A after that point.
|MAX(length(A), length(B)) * 6 |MAX(wordspan(A), wordspan(B)) * 6
|ARITH |ARITH
|- |-
|OP_1SUB |OP_1SUB
@@ -526,7 +550,7 @@ The opcodes OP_ADD, OP_SUB, OP_1ADD and OP_1SUB are redefined in 0xC2 Tapscript
| |
# Pop operands off the stack. # Pop operands off the stack.
# Let B = 1, and continue as OP_SUB. # Let B = 1, and continue as OP_SUB.
|MAX(1, length(A)) * 6 |MAX(wordspan(1), wordspan(A)) * 6
|ARITH |ARITH
|} |}
@@ -549,15 +573,15 @@ The following opcodes have costs below:
! Varops Reason ! Varops Reason
|- |-
| OP_CHECKLOCKTIMEVERIFY | OP_CHECKLOCKTIMEVERIFY
| Length of operand * 2 | wordspan(operand) * 2
| LENGTHCONV | LENGTHCONV
|- |-
| OP_CHECKSEQUENCEVERIFY | OP_CHECKSEQUENCEVERIFY
| Length of operand * 2 | wordspan(operand) * 2
| LENGTHCONV | LENGTHCONV
|- |-
| OP_CHECKSIGADD | OP_CHECKSIGADD
| MAX(1, length(number operand)) * 9 + 500,000 | MAX(wordspan(1), wordspan(number operand)) * 9 + 500,000
| ARITH + COPYING + SIGCHECK | ARITH + COPYING + SIGCHECK
|- |-
| OP_CHECKSIG | OP_CHECKSIG
@@ -624,6 +648,7 @@ Work in progress:
==Changelog== ==Changelog==
* 0.2.2: 2026-06-15: clarify wordspan costs, OP_RIGHT semantics, OP_UPSHIFT unaligned cost, CLTV/CSV bounds, and final success-check cost.
* 0.2.1: 2023-03-27: fix OP_MUL cost to round length(B) up * 0.2.1: 2023-03-27: fix OP_MUL cost to round length(B) up
* 0.2.0: 2025-02-21: change costs to match those in varops budget * 0.2.0: 2025-02-21: change costs to match those in varops budget
* 0.1.0: 2025-09-27: first public posting * 0.1.0: 2025-09-27: first public posting
@@ -660,22 +685,23 @@ using multiple instructions).
For multiplication, the steps break down like so: For multiplication, the steps break down like so:
# Allocate and zero the result: cost = (length(A) + length(B)) * 2 (ZEROING) # Allocate and zero the result: cost = (length(A) + length(B)) * 2 (ZEROING)
# For each word in A: # For each word in A:
#* Multiply by each word in B, into a scratch vector: cost = 6 * ((length(B) + 7) / 8) * 8 (ARITH) #* Multiply by each word in B, into a scratch vector: cost = 6 * wordspan(B) (ARITH)
#* Sum scratch vector at the word offset into the result: cost = 6 * ((length(B) + 7) / 8) * 8 (ARITH) #* Sum scratch vector at the word offset into the result: cost = 6 * wordspan(B) (ARITH)
We increase the length of B here to the next word boundary, using We increase the operand lengths to the next word boundary for the word-span
"((length(B) + 7) / 8) * 8", as the multiplication below makes the loops, using <code>wordspan(n)</code> from [[bip-0440.mediawiki|BIP440]], as
difference of that from the simple "length(B)" significant. the multiplication below makes the difference from the simple byte length
significant.
Note: we do not assume Karatsuba, Toom-Cook or other optimizations. Note: we do not assume Karatsuba, Toom-Cook or other optimizations.
The theoretical cost is: (length(A) + length(B)) * 2 + (length(A) + 7) / 8 * ((length(B) + 7) / 8) * 8 * 12. The theoretical cost is: (length(A) + length(B)) * 2 + wordspan(A) / 8 * wordspan(B) * 12.
However, benchmarking reveals that the inner loop overhead (branch However, benchmarking reveals that the inner loop overhead (branch
misprediction, cache effects on small elements) is undercosted by the misprediction, cache effects on small elements) is undercosted by the
theoretical model. A 2.25× multiplier on the quadratic term accounts for theoretical model. A 2.25× multiplier on the quadratic term accounts for
this, giving a cost of: (length(A) + length(B)) * 3 + (length(A) + 7) / 8 * this, giving a cost of: (length(A) + length(B)) * 3 + wordspan(A) / 8 *
((length(B) + 7) / 8) * 8 * 27. wordspan(B) * 27.
This is slightly asymmetric: in practice an implementation usually finds that This is slightly asymmetric: in practice an implementation usually finds that
CPU pipelining means choosing B as the larger operand is optimal. CPU pipelining means choosing B as the larger operand is optimal.
@@ -684,32 +710,32 @@ CPU pipelining means choosing B as the larger operand is optimal.
For division, the steps break down like so: For division, the steps break down like so:
# Bit shift both operands to set top bit of B (OP_UPSHIFT, without overflow for B): cost = length(A) * 6 + length(B) * 4 # Bit shift both operands to set top bit of B (OP_UPSHIFT, without overflow for B): cost = wordspan(A) * 6 + wordspan(B) * 4
# Trim trailing bytes. This costs according to the number of byte removed, but since that is subtractive on future costs, we ignore it. # Trim trailing bytes. This costs according to the number of byte removed, but since that is subtractive on future costs, we ignore it.
# If B is longer, the answer is 0 already. So assume A is longer from now on (or equal length). # If B is longer, the answer is 0 already. So assume A is longer from now on (or equal length).
# Compare: cost = length(A) * 2 (COMPARING) # Compare: cost = wordspan(A) * 2 (COMPARING)
# Subtract: cost = length(A) * 6 (ARITH) # Subtract: cost = wordspan(A) * 6 (ARITH)
# for (length(A) - NormalizedLength(B)) in words: # for (wordspan(A) - NormalizedLength(B)) in words:
## Multiply word by B -> scratch: cost = NormalizedLength(B) * 6 (ARITH) ## Multiply word by B -> scratch: cost = NormalizedLength(B) * 6 (ARITH)
## Subtract scratch from A: cost = length(A) * 6 (ARITH) ## Subtract scratch from A: cost = wordspan(A) * 6 (ARITH)
## Add B into A (no overflow): cost = length(A) * 6 (ARITH) ## Add B into A (no overflow): cost = wordspan(A) * 6 (ARITH)
## Shrink A by 1 word. ## Shrink A by 1 word.
# OP_MOD: shift A down, trim trailing zeroes: cost = length(A) * 4 # OP_MOD: shift A down, trim trailing zeroes: cost = wordspan(A) * 4
# OP_DIV: trim trailing zeros: cost = length(A) * 4 # OP_DIV: trim trailing zeros: cost = wordspan(A) * 4
Note that the loop at step 6 shrinks A every time, so the *average* cost of Note that the loop at step 6 shrinks A every time, so the *average* cost of
each iteration is (NormalizedLength(B) * 6 + length(A) * 12) / 2. The cost of each iteration is (NormalizedLength(B) * 6 + wordspan(A) * 12) / 2. The cost of
step 6 is: step 6 is:
(length(A) - NormalizedLength(B)) / 8 * (NormalizedLength(B) * 6 + length(A) * 12) / 2 (wordspan(A) - NormalizedLength(B)) / 8 * (NormalizedLength(B) * 6 + wordspan(A) * 12) / 2
The worst case is when NormalizedLength(B) is 0: length(A) * length(A) * 2 / 3. The worst case is when NormalizedLength(B) is 0: wordspan(A) * wordspan(A) * 2 / 3.
The cost for all the steps is: length(A) * 18 + length(B) * 4 + length(A) * length(A) * 2 / 3. The cost for all the steps is: wordspan(A) * 18 + wordspan(B) * 4 + wordspan(A) * wordspan(A) * 2 / 3.