A tamper-evident, on-chain timestamp of what this article said. Captured 2026-08-28 19:41 UTC.
You can now verify a NIST P-256 (secp256r1) signature natively in Bitcoin Script on BSV, inside a 29,583-byte locking script. The compiler-generated baseline was 959 kB. That is a 32× reduction, and every version along the way was broadcast and confirmed on the BSV testnet.
Here is how, and why it matters.
The gap
Bitcoin's signature opcode, OP_CHECKSIG, only understands one elliptic curve: secp256k1. The rest of the security industry does not. The overwhelming majority of tamper-resistant consumer hardware — YubiKey PIV, WebAuthn/FIDO2 security keys, Apple's Secure Enclave, Android's StrongBox — generates and holds keys on the NIST P-256 curve.
A private key that never leaves a YubiKey can produce P-256 signatures on demand. But nothing native on the chain can verify one. If you want a hardware key to authorise a Bitcoin spend directly, you have to express the entire ECDSA verification in Script.
The baseline
This is possible because BSV removed script-size and opcode limits at the 2020 Genesis upgrade and restored a full arithmetic opcode set. @deggen's P-256 script built using @siggi's Runar compiler takes exactly this route — it emits a byte-identical P-256 verifier to Bitcoin Script from a single high-level source, across nine language front-ends. It works, it is self-contained, however it's locking script is 959 kB.
Worse, a transaction spending such an output is itself close to a megabyte, because the OP_PUSH_TX sighash preimage embeds a second full copy of the verifier as its scriptCode.
So the question I set out to answer: how small can a faithful on-chain P-256 verifier be? Faithful meaning it keeps the real security properties — an invalid signature, or a valid signature lifted onto a different transaction, is rejected.
Four ideas
1. A checker, not a computer.
A verifier doesn't have to compute the expensive parts. ECDSA needs w = s⁻¹ mod n and, for a compressed key, a modular square root. Both are supplied as witness data and re-verified cheaply: the script enforces s·w ≡ 1 (mod n) — one multiply, one comparison, which pins w to the unique inverse. The final coordinate test is done projectively (X ≡ r·Z² mod p in Jacobian coordinates), so there is no modular inversion on-chain at all.
2. Bind the transaction in-script.
An OP_CODESEPARATOR before the final checksig shrinks the preimage's scriptCode to a single byte. The BIP-143 preimage is then assembled inside the lock from a handful of small spender-supplied fields — a technique that grows out of my earlier compile-in-script work. One copy is hashed to the message z; the other is bound to the transaction with OP_PUSH_TX. The spending transaction collapses from ~960 kB to about 230 bytes.
3. Generate code for Script, not for a CPU.
Three passes shrink the arithmetic without changing it: a register-scheduling emitter that consumes operands in place instead of copying them and leaving dead temporaries; lazy reduction — because Chronicle-era script integers are effectively unbounded, intermediate products run unreduced and only the output coordinates are normalised; and a fix-point peephole pass over the bytecode. A Jacobian doubling drops from 208 to 111 bytes.
4. The scalar multiplication is the whole cost.
Everything reduces to computing R = u1·G + u2·Q. I evaluated the standard methods measured in compiled bytes, because in Script an operation's cost is dominated by operand access:
- Straus–Shamir windowing — one addition per round, but keeps all 256 doublings → 49.5 kB.
- Lim–Lee comb — since both bases are fixed at lock-generation time, the whole table precomputes off-chain, and the doublings drop from 256 to 86 → 34.5 kB.
- Signed comb — recoding each scalar to signed digits halves the table and lets w = 6 teeth cut the doublings to 43 → 29.6 kB.
P-256 has no efficient endomorphism (unlike secp256k1's GLV), so 43 doublings is close to the arithmetic floor for this construction.
The trajectory
Every stage was assembled into a real funding-and-spend transaction pair, checked against a local re-implementation of the node's full relay policy, and broadcast to the BSV testnet — accepted first try each time.
The spending transaction, at every stage, is around 230 bytes.
It's a real check
Soundness was tested at each stage. A tampered r, a wrong public key, an inconsistent inverse hint, and — critically — a valid signature re-presented against a different transaction are all rejected. That last one is what the OP_PUSH_TX binding buys you: it prevents signature replay across transactions.
Why do we need a YubiKey-backed wallet
Since the Segregated Witness chain-split of 2017, wallet makers have deliberately excluded BSV from the supported coins in their hardware wallet solutions, making it necessary for hacky workarounds to be used. Furthermore, the recent cold wallet hacks plaguing the industry have destroyed trust in hardware solutions. The goal of this exercise is a hardware wallet where a YubiKey is the only spending authority. The device signs the sighash on PIN and touch; a host wallet supplies fee inputs and broadcasts, but — holding no P-256 key — cannot spend the locked output. This gives BSV users a viable, usable alternative to existing hardware wallets using a ubiquitous, trusted solution which has millions of units in the field being used for all kinds of secure solutions.
Because the locking script is generated deterministically from the public key (about sixty curve operations at deposit time — the same order of work as producing one signature), you can still generate a fresh device-generated key for every deposit at no marginal on-chain cost, keeping fund usage unlinkable in-line with Bitcoin's original design. Spending between Yubikey wallets with change outputs would be as indistingushable as P2PKH.
Credits
The elliptic-curve techniques are applied from the standard literature, not invented here — Straus–Shamir interleaving, the Lim–Lee fixed-base comb, and Hedabou et al.'s signed-comb recoding.
The on-chain foundations are prior BSV work:
- @siggi — Rúnar: the compiler used to create the original version of this idea
- @deggen — the baseline verifier, and the demonstration that a complete P-256 check compiles to Script at all.
- OP_PUSH_TX and the Any-S signCtx construction, plus the compile-in-script preimage techniques this extends: my own.
- Native authoring, simulation, and CLI tooling: the BitcoinSX "SX" toolchain.
The contribution here is the systematic compression — the checker-not-computer formulation, the in-script transaction binding, the Script-specific code generator, and the empirical scalar-multiplication study — and the demonstration that a faithful P-256 verifier fits in a routine transaction.
29,583 bytes to verify a signature the chain has no opcode for.
A full IEEE-format paper is available. Testnet transaction IDs resolve on WhatsOnChain (BSV testnet).
Verify it yourself: the manifest is the exact JSON whose
SHA-256 is 5d8bcb98719bf3f7c1986b4d5c39a499df01d16e593c780fa9b0e398ee9b6490; it binds the tweet id, author, post time, and the article title + the SHA-256 of its body. Re-hash the article body to match c1b538dab653808e1bd76a58d389d3ccbfd48571ef79991ace65a1028a5202c7, and read the anchor transaction's OP_RETURN
bsv.cx / x1 / 5d8bcb98719bf3f7c1986b4d5c39a499df01d16e593c780fa9b0e398ee9b6490 — the block's timestamp proves the article said this at or before that time,
with no trust in bsv.cx. Content archived via the X API.