Keep witness stacks in serialized form in CScriptWitness #36521

pull sipa wants to merge 17 commits into bitcoin:master from sipa:202610_witpack changing 76 files +1254 −279
  1. sipa commented at 6:23 AM on October 14, 2026: member

    CScriptWitness stores a witness stack as a std::vector<std::vector<unsigned char>>, which costs a 24-byte vector object and an allocation for every element. This makes it possible to construct transactions that take a large amount of memory after deserialization, while becoming very slow to deserialize.

    This PR stores the stack as its serialization in a single allocation (or none if the stack is empty), with a small header holding its size and the offset of the last element. Witnesses can no longer be modified in place, only allowing replacing them entirely. Access to the individual witness stack elements is done by on-the-fly deserialization instead. CScriptWitness shrinks from 24 to 8 bytes, and CTxIn from 112 to 96 bytes.

    Later commits also introduce stricter checking during deserialization, rejecting the following before fully deserializing:

    • Transactions with more inputs/outputs than are possible in a valid transaction.
    • Transactions with 0 inputs or 0 outputs (applied in P2P, but not RPC etc. for backward compatibility).
    • Transactions/blocks that exceed the 4M weight limit (also only applied in P2P).

    The result is a significant reduction in the worst-case time and memory allocations needed for deserialization, both in valid and invalid transactions. With it, the worst-case memory of a valid deserialized block drops from 106.8 MiB to 6.0 MiB. Memory usage during deserialization can temporarily be higher: up to 7.8 MiB for valid blocks, and up to 13.4 MiB for invalid blocks, which can combine a few temporary vector allocations of up to 5 MB each.

    Deserialization time and memory of the deserialized block, master → PR. Runtimes are medians in milliseconds. Memory usage is in MiB, and ✕ means rejected at deserialization time. Peak memory refers to the memory during deserialization itself, not persisted in the deserialized CBlock object. Numbers over 50 ms, 20 MiB usage, or 100 MiB peak are in bold.

    | block | gcc | clang | mem | peak mem | |---|--:|--:|--:|--:| | Valid blocks | | 1 tx, max 1-byte wit elems | 99.1 → 16.0 | 111.4 → 21.9 | 106.8 → 3.8 | 145.9 → 7.8 | | 1 tx, max empty wit elems | 279.2 → 18.7 | 293.7 → 25.1 | 91.5 → 3.8 | 182.2 → 7.8 | | max txs, 1 txin, 1 txout, 1-item wit | 12.33 → 13.39 | 15.00 → 15.43 | 6.7 → 6.0 | 6.7 → 6.0 | | max txs, 1 txin and 1 txout | 6.43 → 7.04 | 7.87 → 8.30 | 5.8 → 5.6 | 5.8 → 5.6 | | 1 tx, max txins, 1-elem wit | 4.53 → 4.68 | 5.04 → 5.37 | 4.0 → 2.9 | 4.0 → 2.9 | | mainnet block 969729 | 5.30 → 5.47 | 5.97 → 6.20 | 3.5 → 2.9 | 3.5 → 2.9 | | Invalid blocks | | 400k txs, many wit elems, truncated | 84.8 → 16.1 | 95.6 → 21.8 | ✕ → ✕ | 150.7 → 12.6 | | 400k txs, big wit+scriptsig, truncated | 84.0 → 16.1 | 96.0 → 21.7 | ✕ → ✕ | 150.7 → 13.4 | | 1 tx, 2M 1-byte wit elems | 98.0 → 10.6 | 109.9 → 16.2 | 106.8 → ✕ | 145.9 → 7.8 | | 1 tx, 4M empty wit elems | 271.0 → 13.4 | 288.9 → 19.6 | 91.6 → ✕ | 182.2 → 7.8 | | max empty txs | 101.0 → 0.006 | 139.0 → 0.008 | 67.1 → ✕ | 67.1 → 4.8 | | max txs, 1 txin, 1 txout | 27.6 → 7.3 | 34.5 → 8.5 | 23.4 → ✕ | 23.4 → 6.4 |

    So in addition to improving worst cases very significantly, it also provides modest memory usage improvements to the common case. This translates to mildly increasing the effective capacity of memory-bounded data structures that store transactions, like the mempool.

    Some notable changes:

    • By implementing some checks (for inherently consensus-invalid things) in the deserialization logic, behavior may change. In particular, deserialization errors in P2P cause messages to be dropped/ignored, rather than triggering validation or net processing logic (which may include disconnections and/or inserting into reject filters).
    • The rejection of transactions with too many inputs or outputs applies everywhere, including in RPC. That's hopefully a desirable change, but a change in behavior still.
    • Random access to the witness stack elements becomes $\mathcal{O}(n)$. This is not a problem for any validation logic, but may be relevant for the libbitcoinkernel API which exposes an individual element accessor.

    Issue reported by furszy and 0xB10C.

    There are a few possible follow-ups:

    • Enforce the 400k WU policy limit during transaction deserialization when active (instead of the 4M WU consensus limit).
    • Extend this approach to entire CTransaction, allowing all transaction data to be stored packed together in a few allocations (rather than one per input, one per output, one per witness) plus some index structure to access it efficiently. That's a much bigger change, needing abstractions like #35569, but would reduce the maximum memory usage per WU for valid blocks from around 1.5 bytes/WU to 1.1 bytes/WU (compared to ~28 bytes/WU in master).
  2. DrahtBot commented at 6:23 AM on October 14, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Concept NACK purpleKarrot
    Concept ACK furszy, 0xB10C, instagibbs, w0xlt, rkrux
    Stale ACK Yudis-bit

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #36628 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36628.svg"></sub> (net: reimplement private broadcast as jobs isolated from the node by instagibbs)
    • #36608 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36608.svg"></sub> (Reimplement private broadcast as node state-less jobs + package relay support by instagibbs)
    • #36567 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36567.svg"></sub> (psbt: drop Taproot and MuSig2 signatures when adding an input by Ravan002)
    • #36548 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36548.svg"></sub> (net: serve MSG_BLOCK by stripping witnesses in place by l0rinc)
    • #36466 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36466.svg"></sub> (policy: make unstructured Taproot annexes standard by RobinLinus)
    • #36122 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36122.svg"></sub> (BIP460: CISA for Taproot key path spends by fjahr)
    • #36109 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36109.svg"></sub> (test: fix coinbases built on arbitrary parents by l0rinc)
    • #35788 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35788.svg"></sub> (bitcoin-util: Add evalscript subcommand by ajtowns)
    • #35671 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35671.svg"></sub> (mining: add TxCollection to bandwidth-efficiently validate external block templates by Sjors)
    • #35662 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35662.svg"></sub> (script: prevent partial reinitialization of sighash data by l0rinc)
    • #35569 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35569.svg"></sub> (Encapsulation for CTransaction by purpleKarrot)
    • #35558 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35558.svg"></sub> (p2p: Prefill compact blocks by davidgumberg)
    • #35522 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35522.svg"></sub> (refactor: Extract per-message helpers from SendMessages() (move-only) by pablomartin4btc)
    • #35511 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35511.svg"></sub> (RFC: consensus: Make CAmount a class by hodlinator)
    • #31252 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/31252.svg"></sub> (rpc: print P2WSH and P2SH redem Script in getrawtransaction and getblock by polespinasa)
    • #29247 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/29247.svg"></sub> (CAT in Tapscript (BIP-347) by arminsabouri)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • src/test/fuzz/miniscript.cpp: node->Satisfy(satisfier, stack_mal, false)
    • src/test/fuzz/miniscript.cpp: node->Satisfy(satisfier, stack_nonmal, true)
    • src/test/miniscript_tests.cpp: node.Satisfy(satisfier, stack_mal, false)
    • src/test/miniscript_tests.cpp: node.Satisfy(satisfier, stack_nonmal, true)

    <sup>2026-10-22 18:25:11</sup>

  3. sipa force-pushed on Oct 14, 2026
  4. DrahtBot added the label CI failed on Oct 14, 2026
  5. furszy commented at 6:35 AM on October 14, 2026: member

    Obvious concept ACK

  6. 0xB10C commented at 6:35 AM on October 14, 2026: contributor

    Concept ACK

  7. sipa force-pushed on Oct 14, 2026
  8. sipa force-pushed on Oct 14, 2026
  9. DrahtBot removed the label CI failed on Oct 14, 2026
  10. instagibbs commented at 12:13 AM on October 15, 2026: member

    definite concept ACK, this issue with exceptional tx types shows up in a lot of places, like #36015

  11. w0xlt commented at 6:10 AM on October 15, 2026: contributor

    Concept ACK

  12. rkrux commented at 1:06 PM on October 15, 2026: contributor

    Concept ACK

  13. Yudis-bit commented at 1:22 PM on October 17, 2026: none

    In IsWitnessStandard (src/policy/policy.cpp), tx.vin[i].scriptWitness.ToSpans() allocates on the heap for every Taproot input, even though for standard keypath spends (size() == 1) no policy rules apply. Since back() is O(1), checking witness.size() >= 2 and witness.back() upfront before calling ToSpans() avoids that allocation.

    Similarly in the P2WSH check, ToSpans() is used for indexed access up to sizeWitnessStack. Iterating directly over scriptWitness with a counter avoids allocating the vector of spans without doing O(n^2) indexing.

  14. sipa force-pushed on Oct 19, 2026
  15. sipa commented at 1:01 AM on October 20, 2026: member

    @Yudis-bit Thanks, addressed in a separate commit at the end.

  16. Yudis-bit commented at 3:08 AM on October 20, 2026: none

    Tested ACK d3fb8e8 on Windows.

  17. DrahtBot requested review from furszy on Oct 20, 2026
  18. DrahtBot requested review from rkrux on Oct 20, 2026
  19. DrahtBot requested review from instagibbs on Oct 20, 2026
  20. DrahtBot requested review from 0xB10C on Oct 20, 2026
  21. DrahtBot added the label Needs rebase on Oct 22, 2026
  22. bench: add block deserialization benchmarks for synthetic blocks
    Add benchmarks deserializing blocks filled up to the weight limit with
    synthetic transactions of a few shapes that stress different parts of
    deserialization:
    * spends of P2WPKH outputs to P2WPKH outputs (a typical segwit block)
    * the smallest segwit transactions
    * one transaction with as many inputs as fit,
    * one transaction with as many outputs as fit,
    * one transaction whose witness has as many 1-byte elements as fit.
    d10188e66a
  23. script: give CScriptWitness an interface that hides its representation
    Add element access through spans (forward iteration, front(), back(), and
    indexing), conversion to and construction from a vector of elements,
    serialization, and memory usage accounting, so that users no longer need to
    access the stack vector directly. This prepares for a future commit which
    will replace the representation with a more compact & efficient one.
    
    The stack vector itself stays public until the next commits have converted
    its users.
    e41dd47374
  24. test: check CScriptWitness against the vector of vectors it represents
    Add a unit test and a fuzz test that compare CScriptWitness's interface
    (accessors, construction, and serialization) with the
    std::vector<std::vector<unsigned char>> it represents. The unit test
    focuses on element counts and sizes around CompactSize encoding
    boundaries.
    111a1d381d
  25. scripted-diff: use CScriptWitness's interface instead of its stack
    Convert the uses of CScriptWitness::stack for which the new interface
    is a simple substitution. The remaining ones are converted manually in
    the next commits.
    
    -BEGIN VERIFY SCRIPT-
    FILES=$(git grep -l -E '\.stack\b|(scriptWitness|final_script_witness)\.SetNull\(\)' -- src ':!src/script/miniscript.*' ':!src/script/script.h')
    
    # Element count, emptiness, and last element: the witness has size(), empty() and back() itself. Lines that need the
    # stack itself (to compute the size of its serialization without the element count, to pass the last element where a
    # vector is expected, or to index all elements in a loop) are left for the manual changes.
    sed -i -E -e '/GetSizeOfCompactSize|TAPROOT_LEAF_TAPSCRIPT|i < txin\.scriptWitness/b' \
        -e 's/\.stack\.(size|empty|back)\(\)/.\1()/g' $FILES
    
    # Reading an element by index: the witness has operator[] itself. Writes to elements, and loops indexing all elements
    # (which can be quadratic with indexing that is not constant-time), are left for the manual changes.
    sed -i -E -e '/MAX_STANDARD_P2WSH_STACK_ITEM_SIZE|HexStr\(txin\.scriptWitness|\.stack\[[^]]*\]( =|\.resize)|^\s*\+\+/b' \
        -e 's/\.stack\[/[/g' $FILES
    
    # Const references to the stack: refer to the witness itself, which provides the same read-only accessors.
    sed -i -E 's/^(\s*const auto& \w+( = |\{).*)\.stack(\}?;)$/\1\3/' $FILES
    
    # Comparisons of two stacks: compare the witnesses, which are equal exactly when their stacks are.
    sed -i -E '/\.stack [!=]= /s/\.stack\b//g' $FILES
    
    # Serialization of the stack, and its size: serialize the witness itself, which has the same serialization.
    sed -i -E -e 's/ (>>|<<) ([][A-Za-z0-9_.]+)\.stack( <<|;)/ \1 \2\3/' \
        -e 's/(SerializeToVector|UnserializeFromVector)\(s, (\w+)\.stack\)/\1(s, \2)/' \
        -e 's/::GetSerializeSize\(([][A-Za-z0-9_.]+)\.stack\)/::GetSerializeSize(\1)/' $FILES
    
    # Range-for loops over the stack: iterate over the witness. Its elements are spans, returned by value, so they are not
    # bound by reference.
    sed -i -E 's/for \(const auto& (\w+) : ([][A-Za-z0-9_.]+)\.stack\)/for (const auto \1 : \2)/' $FILES
    
    # Clearing the stack (SetNull(), clear() or resize(0)): assign an empty witness. A clear() followed by pushing elements
    # in a loop is left for the manual changes.
    sed -i -E -e '/txin\.scriptWitness\.stack\.clear/b' \
        -e 's/(scriptWitness|final_script_witness)\.SetNull\(\)/\1 = {}/' \
        -e 's/(scriptWitness)\.stack\.(clear\(\)|resize\(0\));$/\1 = {};/' $FILES
    
    # Assigning a vector of elements to the stack: assign a witness constructed from it (moving from the vector is
    # pointless, as the witness is constructed from a const reference).
    sed -i -E 's/(\w+)\.stack = (std::move\((\w+)\)|(.*));$/\1 = CScriptWitness{\3\4};/' $FILES
    
    # Pushing a single element onto an empty witness (in these benchmarks and tests): assign a witness constructed from
    # that element. The one place that pushes onto a non-empty witness (to make a different witness) is left for the
    # manual changes, as are pushes in loops.
    sed -i -E '/ tx\.vin\[0\]\.scriptWitness\.stack\.push_back\(\{5\}\)|Consume/!s/(\w+)\.stack\.push_back\((.*)\);$/\1 = CScriptWitness{{\2}};/' \
        src/bench/block_assemble.cpp src/bench/blockencodings.cpp src/bench/mempool_eviction.cpp src/bench/mempool_stress.cpp src/bench/rpc_mempool.cpp \
        src/test/fuzz/connect_block.cpp src/test/fuzz/txdownloadman.cpp src/test/orphanage_tests.cpp src/test/txdownload_tests.cpp src/test/validation_block_tests.cpp
    
    # IsNull(): use empty(), which the witness provides along with size() and its other container-like accessors.
    sed -i -E -e 's/(scriptWitness|final_script_witness)\.IsNull\(\)/\1.empty()/g' -e 's/witness->IsNull\(\)/witness->empty()/g' \
        $(git grep -l -E '(scriptWitness|final_script_witness)\.IsNull\(\)|witness->IsNull\(\)' -- src)
    -END VERIFY SCRIPT-
    67d5ec76ff
  26. script: let ExecuteWitnessScript take its initial stack by value
    This lets callers move the stack into it, rather than having it be
    copied from a span. This will be used in a future commit.
    376dbf8069
  27. refactor: use CScriptWitness's interface where that needs more than a rename
    Convert the remaining non-test uses of CScriptWitness::stack, which need
    more than a substitution. The script interpreter now converts the
    witness stack to a vector of vectors once, and moves the remaining
    elements into the execution stack, rather than copying them.
    0b4f24be6f
  28. test, bench, fuzz: build witness stacks before constructing a CScriptWitness
    Convert the remaining uses of CScriptWitness::stack in tests, benchmarks,
    and fuzz tests, by constructing witnesses from a separately-built vector
    of elements, rather than modifying the stack in place.
    9e22357e32
  29. streams: add SpanReader::Remaining()
    This returns the data that has not been read yet, as a span referring
    to the underlying data. It will be used in the next commit to find the
    elements of a witness stack within its serialization.
    74b1e69684
  30. script: store witness stacks as their serialization
    Replace CScriptWitness's vector of vectors with a single allocation,
    containing the size of the stack's serialization (4 bytes, little
    endian), followed by that serialization. An empty stack uses no
    allocation at all.
    
    This means a witness stack no longer needs one allocation per non-empty
    element plus one for the outer vector, and no longer has a 24-byte
    overhead per element. A block whose witnesses consist of millions of
    tiny elements thus no longer costs tens of bytes of memory and an
    allocation per byte. It also shrinks CScriptWitness from 24 to 8 bytes,
    and thus CTxIn from 112 to 96 bytes. The downside is that indexing
    becomes O(n) in the distance from the bottom of the stack.
    
    This is the simplest implementation of this representation:
    deserialization still goes through a vector of vectors, and elements
    are found by parsing the serialization with SpanReader and
    ReadCompactSize. Future commits will make both faster.
    6a5eeda000
  31. script: deserialize witness stacks directly into their serialization
    Instead of deserializing a witness stack as a vector of vectors and then
    serializing that again, build the serialization directly while reading,
    with the same checks. This makes deserializing a block whose witness
    consists of millions of 1-byte elements about 10x faster.
    
    To support this, add AppendCompactSize to serialize.h, which cheaply
    appends a CompactSize to a byte vector.
    a96bec2517
  32. script: decode the CompactSizes in witness stacks inline
    Add DecodeCompactSize to serialize.h, which decodes a CompactSize from a
    span without any checks, making it small enough to be inlined.
    
    Use it to find the elements in a CScriptWitness's serialization, which
    is known to be valid as CScriptWitness constructed it. This makes
    iterating over witness stacks 5-7x faster, both for real blocks and for
    blocks whose witnesses consist of millions of 1-byte elements. As a
    bonus, script.h no longer needs streams.h.
    0871946014
  33. script: store the offset of a witness stack's last element
    Add the offset of the last element to the header of CScriptWitness's
    allocation, making back() and accessing the last element by index O(1).
    The last element is used in P2WSH sigop counting, policy checks, and
    BIP352 public key extraction.
    
    This adds 4 bytes to the allocation of every non-empty witness stack,
    but does not change the minimum allocation size of a single-element
    stack, nor the worst-case ratio of memory usage to weight or size.
    dc91fd9364
  34. primitives: reject txs with more inputs or outputs than fit in a block
    When deserializing a transaction, read the input and output counts
    first, and fail before reading the inputs or outputs if more of them
    are claimed than can fit in a block, given their minimum size (41 bytes
    for an input, 9 for an output). Such a transaction can never be valid.
    
    This allows us to allocate the vin and vout vectors immediately, rather
    than sequentially in batches of 5 MB as the std::vector deserializer
    does, because that memory becomes bounded: to 2.3 MB for 24390 inputs,
    and 5.3 MB for 111111 outputs. It is a preparation for further changes
    that rely on this structuring of conditionals.
    
    This affects all places where transactions are deserialized. Through
    RPC, such transactions can no longer be decoded. From P2P, a transaction
    or block containing one is ignored like other malformed messages, rather
    than rejected by validation.
    dbc10d44f9
  35. p2p: reject transactions without inputs or outputs when deserializing
    Add a reject_empty flag to TransactionSerParams, which makes
    deserialization fail as soon as a transaction is found to have no
    inputs or no outputs. Such transactions are never valid, but are
    currently fully deserialized (along with the rest of the message) before
    validation rejects them.
    
    This reduces the memory a peer can make us allocate by sending a block.
    A block full of 10-byte empty transactions was the previous worst case;
    with this change, the worst case becomes 1-input 1-output transactions
    with a significantly lower per-WU memory usage.
    
    The flag is enabled (as TX_WITH_WITNESS_P2P) only where transactions are
    received from P2P peers: tx and block messages, and the transactions in
    cmpctblock and blocktxn messages. Like other malformed messages, these
    are now ignored, rather than, for a block, causing the peer to be
    disconnected, or, for a transaction, being added to the reject filters.
    The functional tests are updated accordingly.
    476a8ba657
  36. policy: iterate over witness stacks directly in IsWitnessStandard
    Instead of converting a witness stack to a vector of spans for the P2WSH
    and Taproot checks, use its size() and back() (which are constant-time)
    and iterate over its elements directly. This avoids an allocation for
    every witness spend that is checked.
    8072be3acb
  37. refactor: use the stream's parameters for compact block transactions
    BlockTransactions and PrefilledTransaction (de)serialized their
    transactions with TX_WITH_WITNESS_P2P, overriding any parameters of the
    stream. Use the stream's parameters instead, like CBlock does, and pass
    TX_WITH_WITNESS where they are serialized and TX_WITH_WITNESS_P2P where
    they are deserialized. This does not change behavior (serialization only
    depends on allow_witness), but makes it possible to deserialize them
    with other parameters.
    85495b700d
  38. p2p: limit the weight of transactions and blocks while deserializing
    Add an optional weight budget to TransactionSerParams. When it is set,
    the budget is decremented while transactions are being deserialized
    (charging the minimum weight of inputs and outputs before allocating
    memory for them), causing failure as soon as the budget is exceeded.
    
    Use this for tx, block, cmpctblock and blocktxn messages, with a budget
    of MAX_BLOCK_WEIGHT minus the header: the transactions in a block,
    cmpctblock or blocktxn message are part of one block (for blocktxn, this
    does not account for those of the block that are already known), and a
    transaction that does not fit in a block cannot be valid.
    
    The P2P message size limit allows blocks of up to 4 MB, which is up to
    16M weight, all of which is currently deserialized before validation
    rejects it (unless a transaction has more inputs or outputs than fit in
    a block). With this change, the weight deserialized from a message is
    at most MAX_BLOCK_WEIGHT minus the header.
    
    Messages exceeding the budget are ignored, like other malformed
    messages, rather than causing the peer to be disconnected for sending an
    invalid block.
    bd51226995
  39. sipa force-pushed on Oct 22, 2026
  40. sipa commented at 6:52 PM on October 22, 2026: member

    I have added changes that also reject transactions/blocks early during deserialization which exceed the 4M WU limit. This reduces the worst-case memory and deserialization time for invalid transactions/blocks further.

  41. DrahtBot removed the label Needs rebase on Oct 22, 2026
  42. fametrano commented at 7:45 AM on October 25, 2026: none

    VerifyWitnessProgram calls witness.ToVectors() before it looks at the witness version, so the valid "1 tx, max empty wit elems" block from the description still allocates about 91 MiB, now during script verification.

    Such a witness is only valid when no script runs on it, as when it spends an unknown witness version, so the copy is never used. As in 8072be3acb, calling ToVectors() only in the branches that reach ExecuteWitnessScript would keep the saving through validation.

    I counted heap in a program that replaces operator new, linked against each build, and took times from regtest nodes running each build. With 3,990,000 empty elements spending a v2 output, deserialization peaks at 7.8 MiB on bd51226995 against 182.7 MiB on its base. VerifyScript then makes a 91.3 MiB copy that the base does not make. The Verify 1 txins bench line goes from 0.02-0.03 ms to 18-24 ms. The block is still accepted faster than on the base (0.04-0.05 s against 0.14-0.20 s).

    I also compared the two builds over RPC and P2P in one functional test (edge cases, test vectors, blocks at the weight limit, PSBTs), and on 400,000 mutated transactions fed to each deserializer. I checked decoding, wtxids, sighashes, weight and sigops against btclib, a parser I maintain. Apart from error messages, the only differences were those the description lists. I can share the code, corpus generator included, as a gist.

  43. purpleKarrot commented at 5:13 PM on October 29, 2026: contributor

    May I dare to NACK this? I understand that keeping the witness data in serialized format considerably improves performance, but as I wrote in #35569 (comment), there sure are hundreds of possibilities for data layouts that may be even more optimized for fast data access.

    Instead pushing 17 commits touching over 1000 lines in 76 files, just to establish an implementation that is proven better but not proven best, should we maybe concentrate on the task that would allow us to make such a change localized in a single place? This would give us the possibility to easily experiment with different data layouts and lead to an even faster implementation?


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-10-30 09:40 UTC