Keep data that nothing uses, and standard templates, byte for byte - #7
Merged
Merged
Conversation
added 2 commits
September 17, 2026 13:53
A script can be shorter without being the same output. A survey of 385 mainnet blocks found two cases where the optimizer was making that mistake: <document> OP_DROP <pubkey> OP_CHECKSIG 121 B -> 35 B <hash> 21e8 OP_SIZE ... OP_DROP OP_CHECKSIG 46 B -> 10 B (a Boost puzzle) OP_2 <key A> <key B> <key A> OP_3 OP_CHECKMULTISIG 201 B -> 136 B The first two carry data: a push no operation takes as an operand and that is not left on the stack. Removing it leaves the script doing the same thing, but the output no longer carries what it was made to carry. Such pushes are now barriers, kept with their encoding; when a region carries data, its one-byte fields and empty pushes are kept too, since they are part of the payload. keepData: false (--drop-unused-data) restores the old behaviour, and dataMinBytes (default 2) sets the length that marks a region as carrying data. The third is a bare multisig with a repeated key: reusing it with OP_OVER saves 65 bytes and stops the output being a bare multisig, which wallets, indexers and relay policy recognise by its exact bytes. P2PKH, P2PK, P2SH, bare multisig and OP_RETURN data outputs are now returned unchanged; templates: false (--rewrite-templates) optimizes them anyway. Finding unused values needs to know which ones an operation consumed, folded operands and PICK/ROLL indices included, so SymState records that on request. No effect on generated contract code: across the 72 mainnet script templates the savings are the same as with the protections off, and the Miller loop, final exponentiation and 19 other module scripts come out byte for byte as before.
examples/mainnet-survey.js samples blocks from the Genesis upgrade to the tip through WhatsOnChain, classifies every output's locking script, groups the non-standard ones into templates and optimizes one script of each. It only reads public data, paces its requests, and resumes where it stopped. On 240 blocks (15,564 transactions, 59,323 outputs, 27.5 MB of locking script): half the outputs are P2PKH holding 2.8% of the bytes, 41.2% of the bytes are OP_RETURN data, and non-standard scripts are 1.9% of outputs but 52.3% of the bytes. The saving is 0.40% of all locking-script bytes, nearly all of it in contracts that check their own transaction: compiled contracts give up a fifth to a third of their bytes, a hand-tuned covenant 5 bytes of 1,341. docs/mainnet-survey.md has the numbers, what they mean for who would use this, and the caveats: one sample, one script per template, locking scripts only, and savings that apply to a contract deployed from the optimized script.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A script can be shorter without being the same output. A survey of 385 mainnet blocks (25,516 transactions, 77,908 outputs) found the optimizer making that mistake on real scripts:
<document> OP_DROP <pubkey> OP_CHECKSIG<pubkey> OP_CHECKSIG <tag> <fields…> OP_2DROP OP_2DROP OP_DROP<hash> 21e8 OP_SIZE … OP_DROP OP_CHECKSIGOP_2 <key A> <key B> <key A> OP_3 OP_CHECKMULTISIGEach rewrite is correct: the script does exactly what it did. But the first three delete what the output exists to carry, and the fourth reuses the repeated key with
OP_OVER, so the output is no longer a bare multisig, which wallets, indexers and relay policy recognise by its exact bytes.Unused data is kept
A push is unused when no operation takes its value as an operand (folded operands and
PICK/ROLLindices included) and it is not left on the stack for code after its region. That is what a data carrier looks like. Such pushes are now barriers: kept byte for byte with their encoding, never crossed, and checked by the proof like any other barrier.dataMinBytes(default 2) or more, every unused push in it is kept, one-byte fields and empty pushes included: those are as much a part of a payload as the rest.keepData: false(--drop-unused-data) restores the old behaviour.report.kept.dataPushescounts what was kept, and a warning says so.aabbccdd aabbccdd OP_EQUAL OP_VERIFY OP_1still folds toOP_1.Finding this needs to know which values an operation consumed, so
SymStaterecords that when asked (trackUses).Standard templates are returned unchanged
P2PKH, P2PK, P2SH, bare multisig and
OP_RETURNdata outputs come back byte for byte, withreport.kept.templatenaming the template.templates: false(--rewrite-templates) optimizes them anyway.No cost to contract code
Run over all 72 script templates from the survey, at
dataMinBytes1, 2, 4 and 8 against the protections off:dataMinBytes: 2(the default): every OP_PUSH_TX contract saves exactly what it saved before. The only scripts that change are the data carriers and the multisigs above, which are now left alone.1costs three contracts 10–14 bytes each;8lets short tags (dylan1,NOTE) be deleted.Byte-identical output, compared by SHA-256, on: the Miller loop (333,031 → 199,147), the final exponentiation (473,562 → 284,713),
field-300.hex,sha256.block,g1.inSubgroup,fp12.mulandec.add. The 19-module harness from script-high-level-modules is unchanged too (74,362 → 57,462 total), with every honest case passing and every refusal refused.Tests
keepData: false); the Boost puzzle keeps its hash while its code still shrinks; used constants still fold; the repeated-key multisig is untouched (and shrinks withtemplates: false); each template is named.npm test: 30/30.Second commit: the survey this came from
examples/mainnet-survey.jsis the tool, anddocs/mainnet-survey.mdthe results. It samples blocks from the Genesis upgrade to the tip through WhatsOnChain, classifies every output's locking script, groups the non-standard ones into templates and optimizes one script of each. It only reads public data, paces its requests (350 ms apart, retried on 429) and resumes where it stopped, socollectcan be run in short sessions.On 240 blocks, 15,564 transactions and 59,323 outputs holding 27.5 MB of locking script:
OP_RETURNdata outputThe saving is 0.40% of all locking-script bytes, almost all of it in contracts that check their own transaction: compiled contracts give up a fifth to a third of their bytes (a 200 KB one from 2026: −32,081 B per output), while the most common hand-tuned covenant gives up 5 bytes of 1,341. A wider 385-block sample gave 0.96% overall and 21% of OP_PUSH_TX contract bytes; how much a sample catches depends on which contract outputs it includes.
The document states the caveats: one sample, one script per template, locking scripts only, and savings that apply to a contract deployed from the optimized script, since these contracts commit to their own code.
The two commits are separate; this one changes no optimizer behaviour (it adds the tool, the document, a README paragraph and
.survey/to.gitignore). I would have put it in its own pull request stacked on this one, but that needed a force push, which is disabled here.