Checking post-quantum signatures in Bitcoin Script

A small experiment. It ran on a private test network with an experimental fork. It is not production software and 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.

Transactions per second

If every transaction on the network used one scheme, how many transactions would fit? This follows a chart by Jonas Nick and its script: 4,000,000 weight units per block, one block every 600 seconds, an average transaction with 2.27 inputs and 2.64 outputs. The P2TR row reproduces his number. The other rows use the witness sizes we measured, which include the checker program because there is no native opcode.

His plot, redrawn with our points added. Higher is better.
Scatter plot of transactions per second against witness bytes per input. Jonas Nick's six points run from P2TR at 6.55 down to SLH-DSA at 0.36. Our points: a hypothetical native opcode at 2.81, SHRINCS in Script stateful at 0.53, stateless at 0.14, and the inline versions at 0.03 and 0.02.
Show as table
SchemeTx per secondTx per blockInputs spent per blockvbytes per tx
P2TR key spend, today6.553,9298,918254
Native opcode, hypothetical2.811,6843,822594
Script, stateful, shared0.533157153,173
Script, stateless, shared0.148719711,534
Script, stateful, inline0.03184154,745
Script, stateless, inline0.02132979,272

The native-opcode row is not measured. It assumes a consensus change that checks the same 660-byte signature with nothing else in the witness. His SHRINCS point sits higher, at 4.13, because it uses a 324-byte signature from a different parameter set. Our signature comes from the pinned specification and is 660 bytes for this key.

The checker program, not the signature, sets the throughput. Moving it into the node would raise the stateful figure about five times. "Inline" means the same checker written out without shared functions. Shared functions raise the stateful figure about seventeen times over that.

How big is a SHRINCS transaction?

One input, two outputs, one SHRINCS signature, using the compact checker with shared functions. These are the real transactions the test network mined.

Stateful spend
1,416 vbytes
5,324 bytes on the wire, 5,663 weight units
Stateless spend
5,099 vbytes
20,056 bytes on the wire, 20,395 weight units
What the bytes are. Same scale for both transactions.
Composition of each transaction in bytes: signature, checker program, control block, and the rest of the transaction Stateful Signature: 660 bytes Checker program: 4,476 bytes Control block: 65 bytes Rest of the transaction (inputs, outputs, counts): 123 bytes 5,324 Stateless Signature: 5,777 bytes Checker program: 14,091 bytes Control block: 65 bytes Rest of the transaction: 123 bytes 20,056
Show as table
PartStateful, bytesStateless, bytes
Signature6605,777
Checker program4,47614,091
Control block6565
Rest of the transaction123123
Total on the wire5,32420,056
Virtual size1,4165,099

The program is the biggest part, not the signature. Fees are paid per vbyte, and witness bytes count for a quarter, so the stateful spend costs about ten times an ordinary payment of around 150 vbytes. Without shared functions the same spends are 24,135 and 34,940 vbytes.

Valid spends mined
14
Tampered spends rejected
128
Tree depths tested
255

Shared functions make it small

The fork lets a program define a function once and call it many times. Without that, every repeated step must be written out in full.

Program size in bytes. Lower is better.
Program size: with shared functions versus without, for the stateful and stateless signature types Stateful Stateful, with functions: 4,476 bytes 4,476 Stateful, without functions: 95,350 bytes 95,350 Stateless Stateless, with functions: 14,091 bytes 14,091 Stateless, without functions: 133,452 bytes 133,452
Show as table
Signature typeWith functionsWithout
Stateful4,47695,350
Stateless14,091133,452

What "with shared functions" means. The fork has two opcodes, OP_DEFINE and OP_INVOKE, that let a program store a piece of code once and run it many times. The checker repeats a few steps thousands of times: one hash-chain step, one Merkle-tree step, one WOTS check. "With shared functions" defines each of those once and calls it. "Without" writes every repetition out in full. Both programs do the same work and accept exactly the same signatures.

Two things differ between the bars, not one. The "with" version also uses OP_BYTEREV, a byte-reversal opcode the fork adds; the "without" version builds byte reversal from slices. Measured on the standalone stateful checker: 95,209 bytes with neither, 52,038 bytes with byte reversal alone, 4,335 bytes with byte reversal and shared functions. So reversal accounts for a bit under half of the baseline size, and functions for nearly all of the rest. The bars above add the 141-byte spending policy to those figures.

The "without" checker itself uses only the restoration opcodes from BIP 440 and BIP 441. The spending policy around it, in both versions, also needs the fork's OP_TX opcode to read the transaction being signed. Every spend on this page therefore depends on OP_TX; only the verifier inside the "without" policy is restoration-only.

Complete transaction size in vbytes, the unit Bitcoin fees are paid in. Lower is better.
Complete spend size: with shared functions versus without Stateful Stateful, with functions: 1,416 vbytes 1,416 Stateful, without functions: 24,135 vbytes 24,135 Stateless Stateless, with functions: 5,099 vbytes 5,099 Stateless, without functions: 34,940 vbytes 34,940
Show as table
Signature typeWith functionsWithout
Stateful1,41624,135
Stateless5,09934,940

For scale: an ordinary Bitcoin payment today is about 150 vbytes. The stateful spend is about ten times that. The signature itself is 660 bytes; the rest is the checker program and the transaction.

The stateful spend uses about 36% of the execution budget the fork allows for a transaction of its size. The stateless spend uses about 60%. Both fit without any tricks.

Where the cost goes

The fork charges every instruction a fixed price: 1,250 units for most, 3,000 for comparisons and arithmetic, 4,000 for a function call. On top of that it charges for data-dependent work, such as 50 units per byte hashed. We expected hashing to dominate. It does not.

Share of the charged execution cost for one stateful spend.
Cost breakdown: fixed per-instruction charge 90 percent, data-dependent charges 10 percent, function body copying under 1 percent Fixed charge per instruction: 90.1% 90% fixed charge per instruction Data-dependent charges (hashing, copying, arithmetic): 9.6% Function body copying: 0.3%
Show as table
PartShare
Fixed charge per instruction90.1%
Data-dependent charges. Hashing is at most 8 points of this.9.6%
Function body copying0.3%

Almost all of the cost is bookkeeping: moving bytes around and branching. The cryptography is a small slice. Two things follow:

The numbers behind the charts

One row per program. "Shared" uses shared functions, "inline" writes everything out, and "unified" is one program that accepts both signature types. Executed opcodes and calls are for one spend. The fixed charge is the sum of each executed opcode's fixed price. Charged units are what the fork actually deducted, which includes the data-dependent part.

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,51092%2.5%
Stateless, shared14,09113,79078,5623571,469106,357,500119,994,38989%58.8%
Stateless, inline133,452092,70401,709123,972,000138,826,87589%9.9%
Unified, shared17,75517,38613,7206225318,566,50020,642,95590%10.9%
Unified, inline228,549024,767025332,694,75034,665,00694%1.5%

"Budget used" is charged units divided by the allowance of 10,000 units per weight unit of the transaction. A smaller program gets a smaller allowance, which is why the compact programs use a larger share even though they do less work.

Every opcode executed by the 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_IF5009901,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_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_LSHIFT113,0003,0003,000
OP_DIV113,0003,0003,000
Total13,70116,35718,542,75022,182,250

The three most common opcodes, PICK, CAT, and DROP, move data around. Together with the small constant pushes and the IF, ELSE, and ENDIF triple they account for most of the executed instructions. SHA256 runs 253 times in both versions. Shared functions cut the executed instruction count by about a sixth, mostly by removing skipped branches. They do not change the number of hashes.

OP_MULTI was not needed

The fork has an instruction called OP_MULTI that applies one operation to a variable number of values. The SHRINCS checker never used it. The one real use we found in the fork's own tests, summing all the output amounts of a transaction, can be done with an existing simpler instruction.

Charged cost of three ways to sum 32 output amounts. Lower is better.
Cost to sum 32 amounts: existing instruction 25,698; OP_MULTI 66,593; unrolled loop 393,961 Existing sum instruction: 31 bytes, 25,698 units Existing instruction 25,698 OP_MULTI: 34 bytes, 66,593 units OP_MULTI 66,593 Unrolled loop: 296 bytes, 393,961 units Unrolled loop 393,961
Show as table
ConstructionProgram bytesCharged units
Existing instruction3125,698
OP_MULTI3466,593
Unrolled loop296393,961

The cost difference is small in practice. The real argument is simpler: OP_MULTI is about 230 lines of interpreter code with several different behaviours hidden behind one instruction, and nothing we found needs it. We recommend deferring it.

What we did not do

Check it yourself

Everything is in the repository. It is private for now, so the links need access. Start with the README's reading guide. The code, the test inputs, the transactions, and the measurements are all there, and a clean build on GitHub reproduces the programs and test results byte for byte.

Figures on this page come from the measured results at tag baseline-2026-09-18 and the later evidence refresh. Execution cost is measured in the fork's own charged units, not seconds.