SHRINCS in Script and Simplicity
We translated Blockstream Research’s Simplicity verifier into GSR Bitcoin Script. Both implementations check the same public keys, messages, and signature fields.
The original experiment uses the SHRINCS BIP draft; this comparison uses Blockstream Research’s construction. They use different hash-tree parameters. The draft’s 48-byte public key stores two root hashes separately. Blockstream’s 32-byte public key stores a hash of both roots. Results from the two constructions are not directly comparable.
Program size
These bars show encoded instructions, without input data or transaction overhead. All three Script variants use shared functions and optimized CAT joins.
Simplicity removes unused branches before encoding. This operation is called pruning. Script keeps its conditional branches.
What about the 50 kB claim?
We previously cited an approximate 50 kB verifier size. We have not verified its program revision, compiler, or size definition.
Our current Simplicity build gives these sizes. One kB equals 1,000 bytes.
| Program bytes | Stateful | Stateless |
|---|---|---|
| Before pruning | 4,811 | 4,813 |
| After pruning | 2,851 | 4,166 |
Pruning alone cannot explain 50 kB: these programs are already below 5 kB before pruning.
These are separate stateful and stateless programs, compiled with SimplicityHL 0.7.2. They exclude transaction data and execution-budget padding.
The upstream Liquid report measures transaction witness size and includes padding. It does not establish the source of the 50 kB figure.
We need the original program and build settings to explain the difference. Until then, 50 kB is an unverified reference.
Measurements
Each language encodes its input fields differently. Simplicity calls its encoded input data the witness.
Stateful
| Implementation | Program bytes | Input bytes | Execution charge or bound |
|---|---|---|---|
| 384,841 | 1,160 | 23,265,133 varops | |
| 118,676 | 1,160 | 15,675,150 varops | |
| 6,870 | 1,160 | 12,379,290 varops | |
| 6,584 | 1,160 | 11,774,652 varops | |
| 6,471 | 1,160 | 11,986,038 varops | |
| 6,581 | 1,160 | 11,771,496 varops | |
| Simplicity · pruned | 2,851 | 1,162 | 33,991,524 milliweight |
Simplicity before pruning: 4,811 program bytes. C and Rust agree on its cost bound.
Stateless
| Implementation | Program bytes | Input bytes | Execution charge or bound |
|---|---|---|---|
| 133,744 | 4,456 | 119,116,431 varops | |
| 44,750 | 4,456 | 45,048,045 varops | |
| 10,585 | 4,456 | 46,653,390 varops | |
| 10,215 | 4,456 | 44,384,370 varops | |
| 10,035 | 4,456 | 45,200,607 varops | |
| 10,202 | 4,456 | 44,379,477 varops | |
| Simplicity · pruned | 4,166 | 4,392 | 83,528,972 milliweight |
Simplicity before pruning: 4,813 program bytes. C and Rust agree on its cost bound.
GSR charges execution in varops. Simplicity gives an upper limit in milliweight. These units do not give a common performance scale.
Both OP_MULTI variants use shared functions and optimized CAT joins. “All uses” groups hashes, joins, and cleanup operations. “Selective hashing” groups only hashes that pass the compiler’s cost estimate.
Script input sizes exclude the length prefixes for stack items. Simplicity input sizes include witness encoding. Pruning can remove unused fields.
Why a small program can require padding
Liquid gives each Simplicity input a budget based on its complete serialized witness stack. It adds 50 free units.
C ≤ 1,000 × (W + 50)
C is the program’s cost bound in milliweight. W is the serialized witness stack size in bytes.
W includes the program, encoded input, control data, item lengths, and any annex. An annex can carry padding.
Sources: witness budget, 50-unit offset, and milliweight conversion.
| Derived from the measured Simplicity checker | Stateful | Stateless |
|---|---|---|
| Program + encoded input, bytes | 4,013 | 8,558 |
| Minimum serialized witness stack, bytes | 33,942 | 83,479 |
For these costs, the minimum is the bound in weight units, rounded up, minus 50. Program and input bytes already contribute to this minimum. Do not add them again.
The Liquid report records 37.755 kWU and 92.450 kWU and explicitly includes padding. Those figures are not encoded program sizes.
Our standalone build differs from those transaction programs. The budget rule explains why padding is needed; it does not reproduce those exact weights.
The GSR budget rule allows 10,000 varops per transaction weight unit. Its checker with shared functions already has enough allowance from its program bytes alone.
The GSR checker with optimized CAT joins needs 1,178 weight units for stateful verification and 4,439 for stateless verification. These are derived budget requirements, not measured transaction weights.
Complete spend comparisons still need transaction-message binding, a committed public key, and the same transaction structure. No complete-spend ratio is established here.
Verification steps
| Step | Script restoration | Simplicity |
|---|---|---|
| Read the input | Check byte lengths and extract fields. | Decode the witness according to its data types. |
| Recover the signature root | Run hash chains and authentication paths with Script instructions. | Run the same hash rules with typed functions and SHA-256 jets. |
| Check the public key | Combine the recovered root with the supplied unused root. Compare the resulting hash. | Apply the same root-combination rule. |
| Remove unused code | Keep conditional branches in the Script. | Remove branches that this input does not use. |
A jet runs a specified Simplicity operation through native code. This verifier uses general hash and arithmetic jets. It has no dedicated SHRINCS jet.
Jets used by these programs
add_32add_64add_8and_32div_mod_32divide_8eq_256eq_32eq_8left_shift_32left_shift_64lt_8modulo_8multiply_32multiply_8or_64right_shift_32right_shift_64sha_256_ctx_8_add_16sha_256_ctx_8_add_32sha_256_ctx_8_add_4sha_256_ctx_8_add_8sha_256_ctx_8_finalizesha_256_ctx_8_initsubtract_32subtract_8verify
Execution time
The table shows the median of 11 runs on one machine. The timers exclude compilation and process startup.
| Timed operation | Stateful, ms | Stateless, ms |
|---|---|---|
| GSR interpreter · functions + CAT joins | 0.529 | 1.851 |
| Simplicity C · evaluation and bounds checks | 1.225 | 3.976 |
| Simplicity C · decode, check, and evaluate | 3.504 | 7.864 |
| Simplicity Rust · Bit Machine execution | 16.559 | 50.847 |
The timers cover different work. Do not use these values to calculate a speed ratio between the languages. The experiment notes define each timer.
Limits
- Positive coverage is one public signature per mode. Other stateful keys, path depths, and valid FORS and XMSS values remain untested.
- Rejection tests show that changed inputs fail. They do not establish acceptance of other valid signatures or prove equivalence for every input.
- These programs receive the public key and message as inputs. A spending policy must commit to the key and calculate the transaction message.
- We have not measured complete transaction sizes or fees. A smaller program does not guarantee a cheaper transaction.
- Compiler choices affect program size. We have not established the smallest possible program in either language.
Source and data
- Recorded measurements and case inventory
- Method, exact pins, commands, and limits
- Pinned upstream verifier
- Experiment source in our private repository
Recorded host: macOS-26.4-arm64-arm-64bit-Mach-O · arm64