Keep witness stacks in serialized form in CScriptWitness #36521

pull sipa wants to merge 13 commits into bitcoin:master from sipa:202610_witpack changing 70 files +989 −237
  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.

    The last commit also rejects transactions without inputs or outputs while deserializing P2P messages, as empty transactions would otherwise become the new worst case. Such messages are now ignored like other malformed messages. With it, the worst-case memory of a deserialized 4 MB block message drops from 112 MB to 24 MB.

    Median deserialization time and memory of the deserialized block, master → PR:

    block gcc (ms) clang (ms) memory (MB)
    4 MB: one witness with 2M 1-byte elements 110.0 → 10.4 122.9 → 15.8 112 → 4.0
    4 MB: one witness with 4M empty elements 317.2 → 13.5 346.1 → 19.4 96 → 4.0
    4 MB: empty transactions 53.7 → 0.002 (rejected) 55.4 → 0.002 (rejected) 70 → none
    4 MB: transactions with 1 input and 1 output 16.3 → 15.8 17.3 → 17.0 24.5 → 23.5
    4M WU: 1 transaction, max inputs with 1-element witnesses 1.96 → 2.37 2.21 → 2.61 3.5 → 3.1
    mainnet block 969729 2.44 → 2.54 2.58 → 2.70 3.6 → 3.0

    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.

    An alternative, or potential follow-up, is applying this approach to CTransaction as a whole rather than on a per-witness basis. Doing so would reduce the worst-case memory-per-weight for a transaction from 1.5 byte/WU (this PR) to 1.1 byte/WU (compared to ~28 byte/WU for master today).

    Issue reported by furszy and 0xB10C.

  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 ACK furszy, 0xB10C, instagibbs, w0xlt, rkrux

    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:

    • #36466 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36466.svg"></sub> (policy: make unstructured Taproot annexes standard by RobinLinus)
    • #36456 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36456.svg"></sub> (Test: Cover malformed and truncated tx encodings by benthecarman)
    • #36417 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36417.svg"></sub> (bip352: verify redeem script and pubkey hash in GetPubKeyFromInput by Yudis-bit)
    • #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)
    • #35713 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35713.svg"></sub> (Remove boost as a unit test runner by rustaceanrob)
    • #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)
    • #35511 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35511.svg"></sub> (RFC: consensus: Make CAmount a class by hodlinator)
    • #35502 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35502.svg"></sub> (refactor: extract per-message helpers from ProcessMessage (move-only) by w0xlt)
    • #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-->

  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. 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.
    ccac453973
  8. 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.
    544acffbe0
  9. 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.
    c2a1fc55bf
  10. 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;/' \
        -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-
    5f2042ce01
  11. 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.
    96a80b3ebb
  12. 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.
    d03e2c1c67
  13. sipa force-pushed on Oct 14, 2026
  14. 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.
    6c10e80a80
  15. 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.
    e4d5bab0ce
  16. 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.
    9a33606e7f
  17. 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.
    72187e94da
  18. 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.
    57856568c1
  19. 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.
    632fd18ad2
  20. 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 bounds the memory a peer can make us allocate by sending a block:
    without the flag, a block of 10-byte empty transactions costs about 17
    bytes of memory (and one allocation per 10 bytes) per byte received.
    With it, the worst case is a block of minimal transactions with one
    input and one output, costing about 6 bytes of memory per byte.
    
    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 causing the peer to be disconnected for
    sending an invalid transaction or block. The functional tests are
    updated accordingly.
    e1cbb976d2
  21. sipa force-pushed on Oct 14, 2026
  22. DrahtBot removed the label CI failed on Oct 14, 2026
  23. 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

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

    Concept ACK

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

    Concept ACK


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-16 12:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me