Checking post-quantum signatures in Bitcoin Script

What a SHRINCS spend costs under different sets of Script opcodes, measured on an experimental fork's private test network. Not production software. It does not make any coins quantum-safe.

Simple summary

The question

Bitcoin's signatures break if large quantum computers arrive. SHRINCS is a proposed replacement. It relies only on hash functions, which quantum computers do not break the same way.

Bitcoin would need a way to check these new signatures. There are two options:

  1. Add a new built-in instruction just for SHRINCS.
  2. Give Bitcoin Script enough general instructions that anyone can write the checker themselves.

The Script Restoration fork (based on BIP 440 and BIP 441) is option two. So we asked: if we write the SHRINCS checker using only that fork's general instructions, does it work, how big is it, and what does it cost to run?

What we did

  1. Wrote the checker in Bitcoin Script. It handles both kinds of SHRINCS signature and agrees with the official Python reference on every test, including all 255 tree depths and every kind of tampered input.
  2. Spent real test coins with it. On a private network running the fork, we locked coins behind the checker and spent them with SHRINCS signatures.
  3. Measured it. Program size, transaction size, and execution cost, with and without the fork's reusable-function feature.
  4. Made it repeatable. Anyone with the repository can rebuild everything from scratch and get the same programs and test results.

Opcodes and compiler techniques

SHRINCS is a hash-based signature scheme. Its verifier recomputes SHA256 hashes in a fixed pattern. We compare the instructions needed to run that verifier and the cost of each approach.

Each Script variant builds on the previous features. Both OP_MULTI variants use shared functions and optimized CAT joins.

Hover, focus, or tap an implementation name to see its full opcode list.

ImplementationOpcodes and techniquesWhere those opcodes exist
A 64-byte Schnorr signature, checked natively.Bitcoin mainnet. Not quantum-safe. The reference row.
Byte-string and arithmetic instructions from BIP 441, under the BIP 440 budget. OP_TX reads the transaction. The verifier repeats its code instead of calling shared functions.BIP 440 and 441 are drafts by Rusty Russell and Julian Moik. OP_TX is in Rusty Russell's 2024 draft and the fork.
Adds OP_BYTEREV to the restored opcodes. It reverses a byte string in one step.Rusty Russell's 2024 covenant-support draft and the fork.
Adds shared functions to the OP_BYTEREV variant. OP_DEFINE stores a function body; OP_INVOKE runs it.Only the fork, added 11 September 2026. No written specification. Known safety problems, listed below.
Keeps shared functions. Removes an unnecessary empty push and CAT operation from each join. No new opcode.A compiler change found during the OP_MULTI comparison. Not yet in the audited compiler.
Adds OP_MULTI wherever the compiler supports it: hashing parts, joining parts, and dropping items.Rusty Russell's 2024 covenant-support draft and the fork. A reviewer has questioned its inclusion.
Uses OP_MULTI only for hashing joins where the cost estimate predicts a saving. Other joins use CAT.The same OP_MULTI opcode, with a different compiler selection rule.
One instruction checks the entire signature inside the node.Hypothetical. Included to estimate the size without a Script verifier.

The same checker was compiled six ways. The restored-opcode and shared-function variants were also mined on the test network. The native SHRINCS estimate uses the signature size alone. "Stateful" and "stateless" are the two SHRINCS signature types: the small one a wallet normally uses, and the large fallback for when signing state is lost.

Transaction size and fee

One input, two outputs, one signature. Fees are paid per vbyte, and witness bytes count for a quarter, so the whole-transaction byte count and the vbyte count differ. The fee columns assume the shown rates and nothing else.

Stateful signature, 660 bytes

Transaction size in vbytes. Lower is better.
Show as table
ImplementationSignatureProgramControl blockWhole tx, bytesWeightvbytesFee at 1 sat/vBFee at 10 sat/vB
64nonenone181520130130 sat1,300 sat
66095,3506596,20096,53924,13524,135 sat241,350 sat
66052,1796553,02753,36613,34213,342 sat133,420 sat
6604,476655,3245,6631,4161,416 sat14,160 sat
6604,130654,9785,3171,3301,330 sat13,300 sat
6604,064654,9125,2511,3131,313 sat13,130 sat
6604,124654,9725,3111,3281,328 sat13,280 sat
660nonenone7791,118280280 sat2,800 sat

Stateless signature, 5,777 bytes

Transaction size in vbytes. Lower is better.
Show as table
ImplementationSignatureProgramControl blockWhole tx, bytesWeightvbytesFee at 1 sat/vBFee at 10 sat/vB
64nonenone181520130130 sat1,300 sat
5,777133,45265139,419139,75834,94034,940 sat349,400 sat
5,777120,74865126,715127,05431,76431,764 sat317,640 sat
5,77714,0916520,05620,3955,0995,099 sat50,990 sat
5,77712,9136518,87819,2174,8054,805 sat48,050 sat
5,77712,6616518,62618,9654,7424,742 sat47,420 sat
5,77712,8676518,83219,1714,7934,793 sat47,930 sat
5,777nonenone5,8966,2351,5591,559 sat15,590 sat

Reading the two charts: with restored opcodes alone, the checker program is 95,350 bytes and the spend costs 24,135 vbytes, about 186 ordinary payments. Byte reversal alone cuts the program almost in half. Functions cut it by a further factor of twelve, to 1,416 vbytes, about 11 ordinary payments. The join fix takes 86 vbytes more off. A native opcode would bring it to 280 vbytes, about twice an ordinary payment. For the stateless type the signature itself is 5,777 bytes, so even a native opcode leaves the spend at 1,559 vbytes.

Execution cost

The fork gives every transaction an execution allowance of 10,000 units per weight unit and charges each opcode a fixed price plus data-dependent extras. A spend that exceeds its allowance is invalid. Interpreter time is the median of 7 runs on one machine with counters off; it is indicative, not a benchmark.

One caution for reading across rows. Each program signs its own transaction, because the script is part of the signed message, and a different message changes how much hash-chain work the signature needs. So the charged units in these two tables include signature-to-signature variation of a few percent. The matched-input tables further down remove that variation.

Stateful

ImplementationCharged unitsAllowanceShare usedInterpreter timeOpcodes executedSHA256 callsFunction callsPeak memoryLargest item
24,150,510965,390,0002.5%1.26 ms16,35725303,624 B660 B
22,761,377533,660,0004.3%0.93 ms15,33525303,624 B660 B
20,577,52856,630,00036.3%0.64 ms13,701253627,971 B2,672 B
18,838,99453,170,00035.4%0.63 ms12,387253627,627 B2,410 B
19,143,88752,510,00036.5%0.64 ms11,884253627,570 B2,398 B
18,837,88453,110,00035.5%0.64 ms12,379253627,621 B2,404 B

Stateless

ImplementationCharged unitsAllowanceShare usedInterpreter timeOpcodes executedSHA256 callsFunction callsPeak memoryLargest item
130,347,1141,397,580,0009.3%3.48 ms87,3041,439017,876 B5,777 B
119,112,4731,270,540,0009.4%3.27 ms78,7331,514017,876 B5,777 B
119,995,961203,950,00058.8%3.14 ms78,5621,46935731,665 B7,411 B
112,114,253192,170,00058.3%2.99 ms72,1941,57435730,490 B6,807 B
120,942,956189,650,00063.8%3.11 ms72,7491,82935730,240 B6,629 B
112,898,411191,710,00058.9%2.98 ms72,6001,60435730,444 B6,764 B

Three things stand out. Restored opcodes, OP_BYTEREV, and shared functions have similar charged costs. Byte reversal and functions mainly reduce program size. The share of the allowance rises from 2.5% to 36.3% for the stateful spend because a smaller transaction gets a smaller allowance. And the stateless spend with shared functions uses 59% of its allowance, which leaves little room for anything else in the same transaction.

Where the charged units go: mined stateful spend with OP_DEFINE + OP_INVOKE.
Fixed charge per instruction: 90.1%90% fixed charge per instruction Data-dependent charges: 9.6% Function body copying: 0.3%

Nine tenths of the charge is the fixed price of instructions, most of them stack shuffling: PICK, CAT, DROP, small pushes, IF and ENDIF. The SHA256 hashing is under a tenth. The cost model prices this checker as bookkeeping, not cryptography. Programs with much skipped code, such as the restored-opcode verifier, also run slower per charged unit than the model predicts, because parsing skipped code is not charged. Both points bear on any recalibration of the fork's prices.

What OP_MULTI changes

OP_MULTI applies one operation to a run-time number of stack items: push the items, push the count, then OP_MULTI OP_SHA256 hashes them all as one message. The checker builds every hash input by joining parts, so this is where the opcode would help if it helped anywhere.

The OP_MULTI comparison exposed a compiler issue. The audited compiler starts every join from an empty push, which costs two extra opcodes per join. The optimized CAT joins remove these instructions without a new opcode. The all-uses variant then adds OP_MULTI wherever supported. The selective-hashing variant uses OP_MULTI only where hashing costs less.

These tables use one public test signature per signature type and run every program on it, so the signature bytes are identical across rows and only the program differs. That is the fair comparison; the transaction tables above cannot give it, because each program there signs a different message.

Matched input, stateful

ProgramProgram bytesCharged unitsChange from shared functionsFixed chargeSHA256 callsOP_MULTI uses
95,20923,029,889+17.2%21,127,0002480
52,03822,226,377+13.1%20,330,5002480
4,33519,645,11517,673,7502480
3,98917,957,817-8.6%16,081,2502480
3,92318,241,221-7.1%16,502,50024868
3,98317,956,698-8.6%16,083,7502482

Matched input, stateless

ProgramProgram bytesCharged unitsChange from shared functionsFixed chargeSHA256 callsOP_MULTI uses
133,311133,585,802+9.2%119,801,7501,5430
120,607119,995,084-1.9%106,294,2501,5430
13,950122,293,006108,187,2501,5430
12,772111,270,250-9.0%97,752,2501,5430
12,520113,227,624-7.4%100,567,2501,543247
12,726111,269,830-9.0%97,772,2501,54312

The join fix removes 8.0% of the stateful program bytes and 8.6% of its charged cost; 8.4% and 9.0% for the stateless type. OP_MULTI everywhere then removes a further 1.7% of bytes but adds 1.6% to the charged cost, 1.8% for the stateless type. OP_MULTI where it is cheaper changes the charged cost by -1,119 and -420 units, from 2 and 12 uses.

The reason is in the cost table. The OP_MULTI byte itself is free, but its count is an ordinary push and pays the fixed 1,250, and the opcode then charges the target's fixed price once per logical operation. Against that, a chain of CATs pays 3 units per byte of every intermediate result. So hashing with OP_MULTI wins once the intermediate bytes of a join exceed about 422, after the count's own push and decoding: three 200-byte parts already cross that line, at 40,630 units against 42,364 on the pinned evaluator. The 16-byte hashes that dominate this checker do not come close, and the few large joins it has, 12 in the stateless program, are too few to matter. OP_MULTI applied to joins that are not hashed, or to cleanups, always pays the count push for nothing under this table.

So the result is specific: a hash-based signature verifier, whose joins are short, does not gain from OP_MULTI. A program that hashes long lists of large items would. All extended programs pass the same 406 acceptance and rejection checks as the shared-function verifier.

Throughput

If every transaction on the network used one scenario, how many would fit per second? Same method and assumptions as Jonas Nick's chart: 4,000,000 weight units per block, one block per 600 seconds, an average transaction with 2.27 inputs and 2.64 outputs. The P2TR row reproduces his figure. These are capacity estimates from size, not measured network throughput.

His plot, with restored opcodes, shared functions, and the native SHRINCS estimate added. The other scenarios are in the table; they sit within a few percent of their neighbours.
Scatter plot of transactions per second against witness bytes per input, with reference schemes and this experiment's scenarios
Show as table
ImplementationWitness bytes per inputTx per secondTx per block
646.553,929
, stateful96,0750.0318
, stateful52,9040.0633
, stateful5,2010.53315
, stateful4,8550.56336
, stateful4,7890.57340
, stateful4,8490.56336
, stateful6602.811,684
, stateless139,2940.0213
, stateless126,5900.0214
, stateless19,9330.1487
, stateless18,7550.1592
, stateless18,5030.1693
, stateless18,7090.1592
, stateless5,7770.48286

His SHRINCS point sits at 4.13 because it uses a 324-byte signature from a different parameter set. Ours is 660 bytes from the pinned specification. The gap between shared functions and a native SHRINCS opcode, about five times, is the cost of checking in Script instead of in the node. The gap between restored opcodes alone and shared functions, about seventeen times, is what the fork's two extra opcode groups buy.

Feasibility

Question
Fits the fork's consensus limits? (4 MB per item, 8 MB stack, 4 MB of executed function bodies, execution allowance)Yes, all typesYes, all typesYes, all typesYes, all typesNot applicable
Standard under the fork's relay policy? (400,000 weight units; leaf 0xc2 has no witness-item size limit)Yes, mined on regtestYes by the same limits, not spent on a nodeYes, mined on regtestYes by the same limits, not spent on a nodeNot applicable
Standard under Bitcoin Core's policy today?No. Core limits tapscript witness items to 80 bytes. Every signature here is larger. The fork exempts its new leaf version from that rule.Would need its own rule
Share of execution allowance, stateful / stateless3% / 9%4% / 9%36% / 59%36% / 64%None
Fee at 10 sat/vB, stateful / stateless241,350 / 349,400 sat133,420 / 317,640 sat14,160 / 50,990 sat13,130 / 47,420 sat2,800 / 15,590 sat
Specification statusDraft BIPs2024 draft textNone. Code only.2024 draft text; inclusion questioned in reviewNone
Open problemsProgram size. Cost model undercharges skipped code.Skipped code remains uncharged. Program size is reduced.Function bodies can come from the witness. A reserved opcode inside a body makes the spend succeed. No call frames. See the design note.Same function-safety issues. Raises charged cost here; saves under 2% of bytes.Consensus change per scheme

Three things apply to every scenario. The output is ordinary Taproot, so its key path remains vulnerable to a quantum computer; a SHRINCS leaf alone does not protect the coins. There is no wallet, no signer state management, and no security proof; the scheme's own specification lists its proof as unfinished. And the numbers come from one experimental fork whose cost table is being recalibrated, so the execution costs will move; the sizes will not.

What was not done

The numbers behind the charts

One row per program in the mined transactions, from reports/multi-scenario.json 253d472623c96e2f, reports/costs.json 1b8e7a5ba4c9fa36. "Shared" uses OP_BYTEREV, OP_DEFINE, and OP_INVOKE. "Inline" uses restored opcodes without these additions. Executed opcodes and calls are for one spend. The fixed charge is the sum of each executed opcode's fixed price.

ProgramScript bytesOf which function bodiesExecuted opcodesFunction callsSHA256 callsFixed chargeCharged unitsFixed shareBudget used
4,4764,14713,7016225318,542,75020,577,51690%36.3%
95,350016,357025322,182,25024,150,51092%2.5%
14,09113,79080,6623571,574108,982,500123,292,10388%60.5%
133,452091,20401,634122,097,000136,472,39489%9.8%
17,75517,38613,7206225318,566,50020,643,00390%10.9%
228,549024,767025332,694,75034,665,03094%1.5%

Every opcode executed by the mined stateful spend, with and without shared functions, sorted by how often it runs. The last two columns multiply the count by the opcode's fixed price.

Show all 51 opcodes
OpcodeCount, with functionsCount, withoutFixed priceFixed charge, withFixed charge, without
OP_PICK2,1042,2571,2502,630,0002,821,250
OP_CAT1,6571,8811,2502,071,2502,351,250
OP_08489391,2501,060,0001,173,750
OP_17898841,250986,2501,105,000
OP_DROP8927521,2501,115,000940,000
OP_38068371,2501,007,5001,046,250
OP_ELSE5009901,250625,0001,237,500
OP_ENDIF5009901,250625,0001,237,500
OP_IF5009901,250625,0001,237,500
push 1 to 75 bytes4937851,250616,250981,250
OP_25234881,250653,750610,000
OP_LESSTHANOREQUAL4814813,0001,443,0001,443,000
OP_ROLL4604401,250575,000550,000
OP_164314201,250538,750525,000
OP_43673551,250458,750443,750
OP_LEFT2993551,250373,750443,750
OP_SHA2562532531,250316,250316,250
OP_9713251,25088,750406,250
OP_SUBSTR1252251,250156,250281,250
OP_LESSTHAN482913,000144,000873,000
OP_81251781,250156,250222,500
OP_VERIFY1441441,250180,000180,000
OP_5159731,250198,75091,250
OP_NUMEQUAL1051053,000315,000315,000
OP_SIZE1041041,250130,000130,000
OP_SWAP1041041,250130,000130,000
OP_781981,250101,250122,500
OP_694571,250117,50071,250
OP_11411031,25051,250128,750
OP_RSHIFT31883,00093,000264,000
OP_ADD52611,25065,00076,250
OP_FROMALTSTACK63431,25078,75053,750
OP_TOALTSTACK63431,25078,75053,750
OP_1038441,25047,50055,000
OP_1242391,25052,50048,750
OP_1437381,25046,25047,500
OP_1335361,25043,75045,000
OP_INVOKE6204,000248,0000
OP_BYTEREV4901,25061,2500
OP_MOD24243,00072,00072,000
OP_MIN2621,25032,5002,500
OP_154211,2505,00026,250
OP_MUL2113,00063,0003,000
OP_SUB1621,25020,0002,500
OP_DEFINE1301,25016,2500
OP_PUSHDATA1001,25012,5000
OP_TX551,2506,2506,250
OP_EQUAL331,2503,7503,750
OP_DEPTH111,2501,2501,250
OP_DIV113,0003,0003,000
OP_LSHIFT113,0003,0003,000
Total13,70116,35718,542,75022,182,250

Check it yourself

Everything is in the repository, which is private for now. Its README has a reading guide. The code, the test inputs, the mined transactions, and the measurements are all there, and a clean build on GitHub reproduces the programs and test results byte for byte. This page is generated from the committed reports by docs/build_page.py; the scenario tables come from reports/multi-scenario.json, which the follow-up audit re-derives and re-executes.

Built from reports/multi-scenario.json 253d472623c96e2f, reports/costs.json 1b8e7a5ba4c9fa36. Sources: SHRINCS specification, the fork, BIP 440, BIP 441, Rusty Russell's 2024 draft, Jonas Nick's throughput script.