diff --git a/bip-0440.mediawiki b/bip-0440.mediawiki
index 409b5a0f47..7b5493ca07 100644
--- a/bip-0440.mediawiki
+++ b/bip-0440.mediawiki
@@ -10,7 +10,7 @@
License: BSD-3-Clause
Discussion: https://groups.google.com/g/bitcoindev/c/GisTcPb8Jco/m/8znWcWwKAQAJ
https://delvingbitcoin.org/t/benchmarking-bitcoin-script-evaluation-for-the-varops-budget-great-script-restoration/2094
- Version: 0.2.0
+ Version: 0.2.1
==Introduction==
@@ -139,6 +139,23 @@ We use the following annotations to indicate the derivation for each opcode:
;OTHER
: all other operations which take a variable-length parameter: cost = 4 per byte written.
+====Byte Lengths and Word Spans====
+
+Cost formulas distinguish script-visible byte lengths from 64-bit word spans.
+length(X) is the script-visible byte length of stack element
+X. wordspan(n) = ((n + 7) / 8) * 8 is a byte count
+rounded up to the next 8-byte boundary; when the argument is a stack element,
+wordspan(X) means wordspan(length(X)).
+
+Unless a formula explicitly uses wordspan(...),
+length(X) refers to the script-visible byte length of X.
+Formulas use wordspan(...) only when the modeled operation
+examines or writes the padded 64-bit word span, for example integer
+conversion, zero testing, numeric comparison, arithmetic with carry, or bit
+operations over words. Costs remain based on script-visible byte lengths when the
+operation copies, hashes, limits, truncates, prepends, or otherwise produces
+exactly the script-visible bytes.
+
Note that COMPARINGZERO is a subset of COMPARING: an implementation must examine every byte of a stack element to determine if the value is 0. This can be done efficiently using existing comparison techniques, e.g. check the first byte, then `memcmp(first, first+1, len-1)`.
Note that LENGTHCONV is used where script interprets a value as a length. Without explicit limits on number size, such (little-endian) values might have to be examined in their entirety to ensure any trailing bytes are zero, implying a COMPARINGZERO operation after the first few bytes.
@@ -157,15 +174,15 @@ The following opcodes demonstrate the approach, with an analysis of how the cost
! Reason
|-
|OP_VERIFY
-|length(A) * 2
+|wordspan(A) * 2
|COMPARINGZERO
|-
|OP_NOT
-|length(A) * 2
+|wordspan(A) * 2
|COMPARINGZERO
|-
|OP_0NOTEQUAL
-|length(A) * 2
+|wordspan(A) * 2
|COMPARINGZERO
|-
|OP_EQUAL
@@ -203,7 +220,7 @@ OP_EQUAL and OP_EQUALVERIFY don't have to examine any data (and the Bitcoin Core
|COPYING
|-
|OP_IFDUP
-|length(A) * 5
+|wordspan(A) * 2 + length(A) * 3
|COMPARINGZERO + COPYING
|-
|OP_DUP
@@ -215,7 +232,7 @@ OP_EQUAL and OP_EQUALVERIFY don't have to examine any data (and the Bitcoin Core
|COPYING
|-
|OP_PICK
-|length(A) * 2 + length(A-th-from-top) * 3
+|wordspan(A) * 2 + length(A-th-from-top) * 3
|LENGTHCONV + COPYING
|-
|OP_TUCK
@@ -223,7 +240,7 @@ OP_EQUAL and OP_EQUALVERIFY don't have to examine any data (and the Bitcoin Core
|COPYING
|-
|OP_ROLL
-|length(A) * 2 + 48 * Value of A
+|wordspan(A) * 2 + 48 * Value of A
|LENGTHCONV + ROLL
|-
|}
@@ -243,57 +260,57 @@ A reasonable implementation (and the current bitcoind C++ implementation) is to
! Varops Budget Cost
|-
|OP_BOOLAND
-|(length(A) + length(B)) * 2
+|(wordspan(A) + wordspan(B)) * 2
|COMPARINGZERO
|-
|OP_BOOLOR
-|(length(A) + length(B)) * 2
+|(wordspan(A) + wordspan(B)) * 2
|COMPARINGZERO
|-
|OP_NUMEQUAL
-|MAX(length(A), length(B)) * 2
+|MAX(wordspan(A), wordspan(B)) * 2
|COMPARING + COMPARINGZERO
|-
|OP_NUMEQUALVERIFY
-|MAX(length(A), length(B)) * 2
+|MAX(wordspan(A), wordspan(B)) * 2
|COMPARING + COMPARINGZERO
|-
|OP_NUMNOTEQUAL
-|MAX(length(A), length(B)) * 2
+|MAX(wordspan(A), wordspan(B)) * 2
|COMPARING + COMPARINGZERO
|-
|OP_LESSTHAN
-|MAX(length(A), length(B)) * 2
+|MAX(wordspan(A), wordspan(B)) * 2
|COMPARING + COMPARINGZERO
|-
|OP_GREATERTHAN
-|MAX(length(A), length(B)) * 2
+|MAX(wordspan(A), wordspan(B)) * 2
|COMPARING + COMPARINGZERO
|-
|OP_LESSTHANOREQUAL
-|MAX(length(A), length(B)) * 2
+|MAX(wordspan(A), wordspan(B)) * 2
|COMPARING + COMPARINGZERO
|-
|OP_GREATERTHANOREQUAL
-|MAX(length(A), length(B)) * 2
+|MAX(wordspan(A), wordspan(B)) * 2
|COMPARING + COMPARINGZERO
|-
|OP_MIN
-|MAX(length(A), length(B)) * 4
+|MAX(wordspan(A), wordspan(B)) * 4
|OTHER
|-
|OP_MAX
-|MAX(length(A), length(B)) * 4
+|MAX(wordspan(A), wordspan(B)) * 4
|OTHER
|-
|OP_WITHIN
-|(MAX(length(C), length(B)) + MAX(length(C), length(A))) * 2
+|(MAX(wordspan(C), wordspan(B)) + MAX(wordspan(C), wordspan(A))) * 2
|COMPARING + COMPARINGZERO
|}
====Rationale====
-Numerical comparison in little-endian numbers involves a byte-by-byte comparison, then if one is longer, checking that the remainder is all zero bytes.
+Numerical comparison in little-endian numbers involves comparing the padded word spans, then if one is longer, checking that the remainder is all zero.
However, OP_MAX and OP_MIN also normalize their result, which means they can't use the optimized comparison routine but must instead track the final non-zero byte to perform truncation.
@@ -331,6 +348,7 @@ Work in progress:
==Changelog==
+* 0.2.1: 2026-06-15: define wordspan notation and clarify byte-length versus word-span costs.
* 0.2.0: 2026-02-21: increase in cost for hashing and copying based on benchmark results.
* 0.1.0: 2025-09-27: first public posting
diff --git a/bip-0441.mediawiki b/bip-0441.mediawiki
index f658bcefdd..fd4de36f27 100644
--- a/bip-0441.mediawiki
+++ b/bip-0441.mediawiki
@@ -9,7 +9,7 @@
Assigned: 2026-03-25
License: BSD-3-Clause
Discussion: https://groups.google.com/g/bitcoindev/c/GisTcPb8Jco/m/8znWcWwKAQAJ
- Version: 0.2.1
+ Version: 0.2.2
Requires: 440
@@ -154,6 +154,13 @@ stack as arbitrary-length little-endian values (instead of CScriptNum):
# OP_IFDUP
# 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 232, 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
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
stack which contains one or more non-zero bytes, fail.``
+This final success check consumes varops budget as wordspan(A) * 2
+(COMPARINGZERO), where A is the remaining stack element. If this
+cost exceeds the remaining transaction varops budget, fail before performing
+the non-zero-byte check.
+
===Enabled Opcodes===
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
annotations in the varops cost field.
+====Byte Lengths and Word Spans====
+
+This BIP uses the length(...) and wordspan(...)
+convention from [[bip-0440.mediawiki|BIP440]]: length(X) is the
+script-visible byte length of stack element X, while
+wordspan(X) 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====
{|
@@ -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).
# If length(A) is greater than value(LEN), truncate A to length value(LEN).
# 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
|-
|OP_LEFT
@@ -229,18 +250,20 @@ annotations in the varops cost field.
# Pop operands off the stack.
# If length(A) is greater than value(OFFSET), truncate A to length value(OFFSET).
# Push A onto the stack.
-|length(OFFSET) * 2
+|wordspan(OFFSET) * 2
|LENGTHCONV
|-
|OP_RIGHT
|129
|[A OFFSET]
-|Extract the right bytes of A, from OFFSET onwards
+|Extract the rightmost OFFSET bytes of A
|
# 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.
-|length(OFFSET) * 2 + value of OFFSET * 3
+|wordspan(OFFSET) * 2 + MIN(Value of OFFSET, length(A)) * 3
|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.
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====
@@ -273,7 +297,7 @@ OP_RIGHT must copy the bytes, which depends on the OFFSET value.
# Pop operands off the stack.
# For each byte in A, replace it with that byte bitwise XOR 0xFF (i.e. invert the bits)
# Push A onto the stack.
-|length(A) * 4
+|wordspan(A) * 4
|OTHER
|-
|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.
# 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.
-|(length(A) + length(B)) * 2
+|(wordspan(A) + wordspan(B)) * 2
|OTHER + ZEROING
|-
|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.
# For each byte in B (the shorter operand): bitwise OR it into the equivalent byte in A (altering A).
# Push A onto the stack.
-|MIN(length(A), length(B)) * 4
+|MIN(wordspan(A), wordspan(B)) * 4
|OTHER
|-
|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.
# For each byte in B (the shorter operand): exclusive OR it into the equivalent byte in A (altering A).
# Push A onto the stack.
-|MIN(length(A), length(B)) * 4
+|MIN(wordspan(A), wordspan(B)) * 4
|OTHER
|}
@@ -347,8 +371,8 @@ OP_DOWNSHIFT (née OP_RSHIFT).
# 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.
# Push A onto the stack.
-|length(BITS) * 2 + (Value of BITS) / 8 * 2 + length(A) * 3. If BITS % 8 != 0, add length(A) * 4
-|LENGTHCONV + ZEROING + COPYING. If BITS % 8 != 0, + OTHER.
+|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 over the shifted result length.
|-
|OP_DOWNSHIFT
|153
@@ -360,7 +384,7 @@ OP_DOWNSHIFT (née OP_RSHIFT).
## 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)).
# 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
|}
@@ -401,7 +425,7 @@ routine as OP_DOWNSHIFT.
# If the final byte overflows, append a single 1 byte.
# Otherwise, truncate A at the last non-zero byte.
# Push A onto the stack.
-|length(A) * 7
+|wordspan(A) * 7
|OTHER + COPYING
|-
|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 byte’s bottom bit as the value of the top bit, and tracking the last non-zero value.
# Truncate A at the last non-zero byte.
# Push A onto the stack.
-|length(A) * 4
+|wordspan(A) * 4
|OTHER
|-
|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.
# Truncate R at the last non-zero byte.
# 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
|-
|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".
# Trim trailing zeroes off the quotient.
# 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
|-
|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".
# Trim trailing zeroes off the remainder.
# 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
|}
@@ -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.
# 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.
-|MAX(length(A), length(B)) * 9
+|MAX(wordspan(A), wordspan(B)) * 9
|ARITH + COPYING
|-
|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.
# Let B = 1, and continue as OP_ADD.
-|MAX(1, length(A)) * 9
+|MAX(wordspan(1), wordspan(A)) * 9
|ARITH + COPYING
|-
|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.
# If there was final underflow, fail validation.
# 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
|-
|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.
# Let B = 1, and continue as OP_SUB.
-|MAX(1, length(A)) * 6
+|MAX(wordspan(1), wordspan(A)) * 6
|ARITH
|}
@@ -549,15 +573,15 @@ The following opcodes have costs below:
! Varops Reason
|-
| OP_CHECKLOCKTIMEVERIFY
-| Length of operand * 2
+| wordspan(operand) * 2
| LENGTHCONV
|-
| OP_CHECKSEQUENCEVERIFY
-| Length of operand * 2
+| wordspan(operand) * 2
| LENGTHCONV
|-
| OP_CHECKSIGADD
-| MAX(1, length(number operand)) * 9 + 500,000
+| MAX(wordspan(1), wordspan(number operand)) * 9 + 500,000
| ARITH + COPYING + SIGCHECK
|-
| OP_CHECKSIG
@@ -624,6 +648,7 @@ Work in progress:
==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.0: 2025-02-21: change costs to match those in varops budget
* 0.1.0: 2025-09-27: first public posting
@@ -660,22 +685,23 @@ using multiple instructions).
For multiplication, the steps break down like so:
# Allocate and zero the result: cost = (length(A) + length(B)) * 2 (ZEROING)
# For each word in A:
-#* Multiply by each word in B, into a scratch vector: cost = 6 * ((length(B) + 7) / 8) * 8 (ARITH)
-#* Sum scratch vector at the word offset into the result: 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 * wordspan(B) (ARITH)
-We increase the length of B here to the next word boundary, using
-"((length(B) + 7) / 8) * 8", as the multiplication below makes the
-difference of that from the simple "length(B)" significant.
+We increase the operand lengths to the next word boundary for the word-span
+loops, using wordspan(n) from [[bip-0440.mediawiki|BIP440]], as
+the multiplication below makes the difference from the simple byte length
+significant.
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
misprediction, cache effects on small elements) is undercosted by the
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 *
-((length(B) + 7) / 8) * 8 * 27.
+this, giving a cost of: (length(A) + length(B)) * 3 + wordspan(A) / 8 *
+wordspan(B) * 27.
This is slightly asymmetric: in practice an implementation usually finds that
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:
-# 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.
# 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)
-## Subtract scratch from A: cost = length(A) * 6 (ARITH)
-## Add B into A (no overflow): cost = length(A) * 6 (ARITH)
+## Subtract scratch from A: cost = wordspan(A) * 6 (ARITH)
+## Add B into A (no overflow): cost = wordspan(A) * 6 (ARITH)
## 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
-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:
- (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.