From 503939b53d032d8d623eb3367cb0aae68c481947 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 15 Jun 2026 21:18:09 +0200 Subject: [PATCH 1/2] BIP-0440: clarify wordspan costs --- bip-0440.mediawiki | 58 ++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 20 deletions(-) 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 From e840999d2dc3e19ea0f9bdda70b977ba6f666e6f Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 15 Jun 2026 21:26:51 +0200 Subject: [PATCH 2/2] 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. --- bip-0441.mediawiki | 118 +++++++++++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 46 deletions(-) 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.