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.

The four scenarios

SHRINCS is a hash-based signature scheme. Checking one signature means recomputing a few thousand SHA256 hashes in a fixed pattern. The question is what Bitcoin Script would need to do that check, and what a spend costs under each answer.

ScenarioOpcodes the checker needs beyond today's BitcoinWhere those opcodes exist
Today: P2TR key spendNone. A 64-byte Schnorr signature, checked natively.Bitcoin mainnet. Not quantum-safe. The reference row.
A. Restoration onlyThe byte-string and arithmetic opcodes of BIP 441 under the BIP 440 budget, for the checker. OP_TX, for the spending policy to read the transaction.BIP 440 and 441 are drafts by Rusty Russell and Julian Moik. OP_TX is in Rusty Russell's 2024 draft and in the fork.
B. Restoration + byte reversalScenario A plus OP_BYTEREV, which reverses a byte string in one step.Rusty Russell's 2024 covenant-support draft and the fork.
C. Restoration + byte reversal + functionsScenario B plus OP_DEFINE and OP_INVOKE, which store a piece of code once and run it many times.Only the fork, added 11 September 2026. No written specification. Known safety problems, listed below.
C′. C with the join fixScenario C compiled without a wasted empty push at the start of every join. No new opcode.A compiler fix found while building scenario E. Not yet in the audited compiler.
E. C′ + OP_MULTIScenario C plus OP_MULTI, which applies one operation to a run-time number of stack items. E uses it everywhere it applies. E′ uses it only for hashing, and only where the cost table makes it cheaper.Rusty Russell's 2024 covenant-support draft and the fork. A reviewer has already questioned its inclusion.
D. Native SHRINCS opcodeOne new opcode that checks the whole signature inside the node.Nowhere. Hypothetical, included to show the floor.

Scenarios A, B, C, C′, E, and E′ were measured with the same checker compiled six ways. A and C were also mined on the test network. Scenario D is computed from 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.
Today: P2TR key spend: 130 vbytesToday: P2TR key spend130 A. Restoration only: 24,135 vbytesA. Rest. only24,135 B. Restoration + byte reversal: 13,342 vbytesB. Rest. + byte reversal13,342 C. Restoration + byte reversal + functions: 1,416 vbytesC. + functions1,416 C′. C with the join fix: 1,330 vbytesC′. C + join fix1,330 E. C′ + OP_MULTI everywhere: 1,313 vbytesE. C′ + OP_MULTI everywhere1,313 E′. C′ + OP_MULTI where it is cheaper: 1,328 vbytesE′. C′ + OP_MULTI selective1,328 D. Native SHRINCS opcode (hypothetical): 279 vbytesD. Native SHRINCS opcode279
Show as table
ScenarioSignatureProgramControl blockWhole tx, bytesWeightvbytesFee at 1 sat/vBFee at 10 sat/vB
Today: P2TR key spend64nonenone179518130130 sat1,300 sat
A. Restoration only66095,3506596,20096,53924,13524,135 sat241,350 sat
B. Restoration + byte reversal66052,1796553,02753,36613,34213,342 sat133,420 sat
C. Restoration + byte reversal + functions6604,476655,3245,6631,4161,416 sat14,160 sat
C′. C with the join fix6604,130654,9785,3171,3301,330 sat13,300 sat
E. C′ + OP_MULTI everywhere6604,064654,9125,2511,3131,313 sat13,130 sat
E′. C′ + OP_MULTI where it is cheaper6604,124654,9725,3111,3281,328 sat13,280 sat
D. Native SHRINCS opcode (hypothetical)660nonenone7771,116279279 sat2,790 sat

Stateless signature, 5,777 bytes

Transaction size in vbytes. Lower is better.
Today: P2TR key spend: 130 vbytesToday: P2TR key spend130 A. Restoration only: 34,940 vbytesA. Rest. only34,940 B. Restoration + byte reversal: 31,764 vbytesB. Rest. + byte reversal31,764 C. Restoration + byte reversal + functions: 5,099 vbytesC. + functions5,099 C′. C with the join fix: 4,805 vbytesC′. C + join fix4,805 E. C′ + OP_MULTI everywhere: 4,742 vbytesE. C′ + OP_MULTI everywhere4,742 E′. C′ + OP_MULTI where it is cheaper: 4,793 vbytesE′. C′ + OP_MULTI selective4,793 D. Native SHRINCS opcode (hypothetical): 1,559 vbytesD. Native SHRINCS opcode1,559
Show as table
ScenarioSignatureProgramControl blockWhole tx, bytesWeightvbytesFee at 1 sat/vBFee at 10 sat/vB
Today: P2TR key spend64nonenone179518130130 sat1,300 sat
A. Restoration only5,777133,45265139,419139,75834,94034,940 sat349,400 sat
B. Restoration + byte reversal5,777120,74865126,715127,05431,76431,764 sat317,640 sat
C. Restoration + byte reversal + functions5,77714,0916520,05620,3955,0995,099 sat50,990 sat
C′. C with the join fix5,77712,9136518,87819,2174,8054,805 sat48,050 sat
E. C′ + OP_MULTI everywhere5,77712,6616518,62618,9654,7424,742 sat47,420 sat
E′. C′ + OP_MULTI where it is cheaper5,77712,8676518,83219,1714,7934,793 sat47,930 sat
D. Native SHRINCS opcode (hypothetical)5,777nonenone5,8946,2331,5591,559 sat15,590 sat

Reading the two charts: under scenario A 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 279 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

ScenarioCharged unitsAllowanceShare usedInterpreter timeOpcodes executedSHA256 callsFunction callsPeak memoryLargest item
A. Restoration only24,150,510965,390,0002.5%1.28 ms16,35725303,624 B660 B
B. Restoration + byte reversal22,761,377533,660,0004.3%0.92 ms15,33525303,624 B660 B
C. Restoration + byte reversal + functions20,577,52856,630,00036.3%0.67 ms13,701253627,971 B2,672 B
C′. C with the join fix18,838,99453,170,00035.4%0.63 ms12,387253627,627 B2,410 B
E. C′ + OP_MULTI everywhere19,143,88752,510,00036.5%0.66 ms11,884253627,570 B2,398 B
E′. C′ + OP_MULTI where it is cheaper18,837,88453,110,00035.5%0.65 ms12,379253627,621 B2,404 B

Stateless

ScenarioCharged unitsAllowanceShare usedInterpreter timeOpcodes executedSHA256 callsFunction callsPeak memoryLargest item
A. Restoration only130,347,1141,397,580,0009.3%3.62 ms87,3041,439017,876 B5,777 B
B. Restoration + byte reversal119,112,4731,270,540,0009.4%3.27 ms78,7331,514017,876 B5,777 B
C. Restoration + byte reversal + functions119,995,961203,950,00058.8%3.18 ms78,5621,46935731,665 B7,411 B
C′. C with the join fix112,114,253192,170,00058.3%2.97 ms72,1941,57435730,490 B6,807 B
E. C′ + OP_MULTI everywhere120,942,956189,650,00063.8%3.26 ms72,7491,82935730,240 B6,629 B
E′. C′ + OP_MULTI where it is cheaper112,898,411191,710,00058.9%3.00 ms72,6001,60435730,444 B6,764 B

Three things stand out. The charged work barely changes across A, B, and C: functions and byte reversal compress the program, not the computation. The share of the allowance rises from 2.5% to 36.3% for the stateful spend not because C does more work but because a smaller transaction gets a smaller allowance. And the stateless spend under C uses 59% of its allowance, which leaves little room for anything else in the same transaction.

Where the charged units go, stateful spend, scenario C, as mined.
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 a lot of skipped code, as in scenario A, 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.

Building the scenario exposed something else. The audited compiler starts every join from an empty push, which costs two extra opcodes per join. Fixing that needs no new opcode, and it is scenario C′. Scenario E then adds OP_MULTI everywhere it applies. Scenario E′ adds it only for hashing, and only where the cost table says it is cheaper.

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 CFixed chargeSHA256 callsOP_MULTI uses
A. Restoration only95,20923,029,889+17.2%21,127,0002480
B. Restoration + byte reversal52,03822,226,377+13.1%20,330,5002480
C. Restoration + byte reversal + functions4,33519,645,11517,673,7502480
C′. C with the join fix3,98917,957,817-8.6%16,081,2502480
E. C′ + OP_MULTI everywhere3,92318,241,221-7.1%16,502,50024868
E′. C′ + OP_MULTI where it is cheaper3,98317,956,698-8.6%16,083,7502482

Matched input, stateless

ProgramProgram bytesCharged unitsChange from CFixed chargeSHA256 callsOP_MULTI uses
A. Restoration only133,311133,585,802+9.2%119,801,7501,5430
B. Restoration + byte reversal120,607119,995,084-1.9%106,294,2501,5430
C. Restoration + byte reversal + functions13,950122,293,006108,187,2501,5430
C′. C with the join fix12,772111,270,250-9.0%97,752,2501,5430
E. C′ + OP_MULTI everywhere12,520113,227,624-7.4%100,567,2501,543247
E′. C′ + OP_MULTI where it is cheaper12,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 417: 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 scenario C.

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, redrawn with scenarios A, C, and D 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
ScenarioWitness bytes per inputTx per secondTx per block
Today: P2TR key spend646.553,929
A. Restoration only, stateful96,0750.0318
B. Restoration + byte reversal, stateful52,9040.0633
C. Restoration + byte reversal + functions, stateful5,2010.53315
C′. C with the join fix, stateful4,8550.56336
E. C′ + OP_MULTI everywhere, stateful4,7890.57340
E′. C′ + OP_MULTI where it is cheaper, stateful4,8490.56336
D. Native SHRINCS opcode (hypothetical), stateful6602.811,684
A. Restoration only, stateless139,2940.0213
B. Restoration + byte reversal, stateless126,5900.0214
C. Restoration + byte reversal + functions, stateless19,9330.1487
C′. C with the join fix, stateless18,7550.1592
E. C′ + OP_MULTI everywhere, stateless18,5030.1693
E′. C′ + OP_MULTI where it is cheaper, stateless18,7090.1592
D. Native SHRINCS opcode (hypothetical), 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 scenario C and scenario D, about five times, is the cost of checking in Script instead of in the node. The gap between A and C, about seventeen times, is what the fork's two extra opcode groups buy.

Feasibility

QuestionA. Restoration onlyB. + byte reversalC. + functionsE. + OP_MULTID. Native opcode
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,790 / 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.As A, halvedFunction bodies can come from the witness. A reserved opcode inside a body makes the spend succeed. No call frames. See the design note.As C. 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 at commit 33b1f45: multi-scenario.json cce774f31d85, costs.json 29ca417588eb. "Shared" is scenario C, "inline" is scenario A. 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
Stateful, shared4,4764,14713,7016225318,542,75020,577,52890%36.3%
Stateful, inline95,350016,357025322,182,25024,150,52292%2.5%
Stateless, shared14,09113,79078,5623571,469106,357,500119,995,77289%58.8%
Stateless, inline133,452089,10401,529119,472,000133,173,21990%9.5%
Unified, shared17,75517,38613,7206225318,566,50020,643,01590%10.9%
Unified, inline228,549024,767025332,694,75034,665,01894%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_IF5009901,250625,0001,237,500
OP_ELSE5009901,250625,0001,237,500
OP_ENDIF5009901,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_TOALTSTACK63431,25078,75053,750
OP_FROMALTSTACK63431,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_LSHIFT113,0003,0003,000
OP_DEPTH111,2501,2501,250
OP_DIV113,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 at commit 33b1f45: multi-scenario.json cce774f31d85, costs.json 29ca417588eb. Sources: SHRINCS specification, the fork, BIP 440, BIP 441, Rusty Russell's 2024 draft, Jonas Nick's throughput script.