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.