Post-quantum readiness on Ergo, measured: exposure, a no-fork hash-based spend, and what it costs

Three things this post asks. Re-run the UTXO scan on a synced node and post its aggregate tables. Maintainers: is a native hash-based verifier wanted, and which evidence first; and which P2SH box script is canonical, context variable 126 or 1. Everything else is the evidence for those.

In short

  • Nothing for holders to do today. No machine exists that can use any of this, and moving coins to a new P2PK address does not change exposure on Ergo. The cheap work is worth doing now because migrations take years.
  • At height 753,934 (around May 2022, derived from the block time; our node is not synced yet), 96.5% of non-protocol ERG sat in boxes whose script contains a secp256k1 key, and 95.9% of non-protocol ERG was plain P2PK. This is structural: a P2PK address is the key. Re-scans at 778,668 and 783,047 gave 96.1%. The share at the tip is not measured; section 1 says what we expect and how to check.
  • A Winternitz one-time-signature (WOTS) verifier written in plain ErgoScript spends a box on unmodified nodes: a 6.0.6 devnet and a 6.0.1 public-testnet node. A corrupted signature is rejected by the script check; the valid one is mined. No fork.
  • Code shared with a static page made five spends on the public testnet, two WOTS and three P2SH, each after its forged copy was rejected; two of the five were the built page driven in a DOM emulator. The ids are in the repository, and one funded box is left for you to attack.
  • Capacity, derived from measured costs for single-input spends: about 154 WOTS spends per block against about 596 P2PK spends, 3.9x. The script-cost ratio is about 97x and is not a capacity figure.

Thread 257 (2020) asked for a post-quantum replacement for proveDlog and for sizes, and no sizes were posted in that thread. This measures a different path: a one-time hash-based signature in today’s ErgoScript. The hash-based signatures NIST has standardized (XMSS and SLH-DSA on WOTS+, LMS on LM-OTS; SP 800-208 and FIPS 205, external to this work) are built on Winternitz-type one-time signatures of the same shape.

Prepared in the open skunkyard repository (github.com/cafebedouin/skunkyard) with Claude (Anthropic; Fable 5.1 and Opus 5.5), which ran the scans, the devnet and the testnet; Gemini (Google) ran the first verifier pilot and Grok (xAI) the literature check. Tools: sigma-state 6.0.7, ergo-6.0.6, the peeryard devnet rig, Fleet 0.12, sigmastate-js 0.6.3, and a public testnet node running 6.0.1. Measured figures are quoted from printed output kept in the repository; derived figures are labeled; claims from reading code or documents are listed with their sources in notes/2026-10-01-reads.md. Shor against secp256k1 is the same problem for Ergo as for Bitcoin and is not re-estimated here.

1. Exposure is structural

A P2PK address is the serialized compressed public key with no hash step (ErgoAddress.scala, P2PKAddress; read, and the documented mainnet example decodes to a 33-byte point). The box’s proposition bytes carry the same point, so every P2PK box exposes its key from creation. Bitcoin’s hashed addresses expose a key when it is reused, which is why Bitcoin’s exposed share is reported between 25% and 36% depending on definition, with about 1.9 of 7.1 million BTC structural (P2PK and P2TR at rest); sources and their caveats in LITERATURE.md.

We measured with a full UTXO-set scan: a walk of the node’s authenticated AVL+ tree from the stored root, every node hash recomputed (0 mismatches over 2,985,403 nodes), every box parsed with the node’s own serializer and its ErgoTree classified. At height 753,934:

Lock type Boxes ERG Share of non-protocol ERG
Plain P2PK 1,463,099 51,523,423 95.93%
Mining-reward script (carries the miner key) 15,722 276,008 0.51%
Script contains a proveDlog-shaped node (mostly a key built at spend time from a register) 6,637 51,967 0.10%
Exposed, total 1,485,458 51,851,398 96.54%
Scripts with no key found (upper bound on unexposed) 7,233 1,856,981 3.46%
P2SH 0 0 0%

“Exposed” means the category rule in q1/README.md, not that one person holds the key. Protocol boxes (emission, the key-locked foundation boxes, the no-premine proof, and after EIP-27 the re-emission contracts) are excluded from the shares; counting the foundation boxes as exposed gives 96.62% (derived). Limits the scan cannot see: keys leaked off chain (xpubs, signed messages, a key revealed by spending some other box), so the share is a floor on its own definition; registers were inspected for protocol boxes only, so the “no key found” row is an upper bound; the creation height is set by the box creator (pools stamp 0 on re-emission boxes). The re-scans at 778,668 and 783,047 confirm the re-emission contracts classify as protocol and give 96.05% and 96.10%. Two more printed facts from 783,047: the twenty largest exposed boxes together held 13,997,363 ERG, 26.2% of exposed ERG (the aggregate is printed, the list is not); and at 753,934, 9.16M exposed ERG had a creation height more than 525,600 blocks old, about two years at the block time (derived), while the storage-rent bucket was empty by construction since the chain was not four years old. Tokens: 38,700 distinct token ids sat in 154,360 exposed boxes.

What we expect at the tip, and why you should check. Between the first and third scans the “no key found” bucket grew from 1.86M to 2.17M ERG as contract value grew, and coins emitted after 783,047 are outside every table here. P2PK remains the address type every wallet issues (we found no wallet that issues a hash-hidden address; code reads, not a survey). Our expectation is that the exposed share at the tip is still above 95% of non-protocol ERG; whether four years of DeFi and bridge contracts have moved it below is exactly what a scan at the tip answers, and ours will arrive when the node finishes syncing. Independent tables are worth more than ours.

To run it. Needs a JDK, coursier, 4 GB of heap, ergo-6.0.6.jar (checked by hash) and the node stopped while its state/ directory is copied. q1/run.sh (40 lines) copies that directory and scans the copy; it never opens wallet/ or the keystore and makes no network connection itself (coursier fetches the compiler once). At 1.65M boxes the copy took 48 s and the scan 103 s; the tip is larger and untimed. Details in q1/README.md.

bash q1/run.sh <ergo data dir>/state     # copies state/, scans the copy, deletes it
bash q1/check-header.sh                  # node running again: the scanned root against the header's stateRoot

The scan exits non-zero unless the recomputed root equals the stored root, no node label mismatches, and all boxes sum to exactly 97,739,925 ERG (the genesis total). Please post only the by-category table, the protocol rows, the check lines, and the commit you ran. The scan does not print the largest boxes unless asked, and that list has no place in this thread.

2. A hash-based spend already works, with no fork

q2/wots-constant.es is a WOTS verifier in ErgoScript using blake2b256, byte slicing, fold and flatMap (the flatMap form needs the 6.0 compiler), with one context variable carrying the signature and the 32-byte public-key commitment compiled in as a constant, so every key has its own P2S address and a box is funded by a plain send with no registers. At n=32, w=16: 67 hash chains, 64 for the message and 3 for the checksum; signature 2,144 bytes; tree 840 bytes. Use n=32; a 16-byte hash gives about 64-bit preimage security under Grover, so the n=16 rows are cost curves only. The signed message is blake2b256(SELF.id ++ OUTPUTS bytes) truncated to n bytes, which binds the signature to this box and every output. Limits: this message omits the other inputs and data inputs (a script cannot see messageToSign; it can see INPUTS, so a message that covers them is possible and unmeasured), so the pilot is single-input; the tree is ErgoTree v0; the key is strictly one-time, and a second signature on a different message is key reuse, which lets a WOTS signature be forged, so a stuck transaction is never re-signed and a fee bump must spend the output instead; and the chain hashes carry no domain separation (no key, chain or step prefix as in LM-OTS), which an opcode proposal should add from the start.

Executed on a one-node peeryard devnet with ergo-6.0.6. The earlier R4-committed verifier wots.es, at block versions 3 and 4: forged spends rejected at script cost 1,239 to 1,256, valid spends confirmed at 4,488 bytes. The per-key wots-constant.es at version 4, two runs: a signature with one flipped byte rejected (Scripts of all transaction inputs should pass verification ... Success((false,37592))), the valid spend confirmed; 2,340 bytes; node cost 49,792 under devnet parameters, of which the local script evaluation is 37,592. That rejection cost is itself a property: the per-key form has no early exit, so rejecting a forgery costs the node a full verification (37,592) where the R4 form rejected at about 1,250 and a P2SH spend at 569, and the node does that work for a transaction that pays no fee. In the interpreter harness (sigma-state 6.0.7, script version 3), every parameter set tried (Lamport n=16/32; WOTS n=16/32 at w=4/16/256) accepts a valid and rejects a corrupted signature within the block and relay limits.

Prior work on this forum: thread 257 above; thread 3407 verified Schnorr in ErgoScript and was revised by kushti in 2023; EIP-0045 is the nearest open native-verifier proposal; it verifies STARKs. We found no earlier hash-based signature verifier for ErgoTree in the forum, ergoplatform’s GitHub, ePrint or the web; Telegram and Discord were not searched (LITERATURE.md).

3. Capacity: about 4x for single-input spends

The node charges every transaction a fixed cost before any script runs: 10,000 units of interpreter initialization plus inputCost per input and outputCost per output (ErgoTransaction.scala, validateStateful; read). The devnet corroborates it: node cost minus local script cost was exactly 12,200 on every WOTS run (q2/RESULT.md). Script-cost comparisons miss this charge, which dominates a Schnorr spend.

Derived, for one input and two outputs, worst-case message, mainnet parameters at height 1,885,184 (inputCost 2,407, outputCost 298, block cost 8,001,091, block size 1,271,009):

Spend Node cost Per block, cost-bound Per block, size-bound
proveDlog (plain P2PK) 403 (harness) + 13,003 = 13,406 596 4,367 at the 291 bytes of a two-output testnet spend
WOTS n=32 w=16, ErgoScript 38,890 (harness, worst case) + 13,003 = 51,893 154 542 at the 2,345-byte testnet spend

So single-input spends: 3.9x. There is no measured multi-input figure; this verifier’s message does not cover a second input, and P2PK transactions that batch inputs pay the fixed charge once, so the gap per input is larger. The script-cost ratio (39,077 / 403 for wots.es, 38,890 / 403 for the per-key form, about 97x) is real and is not a capacity figure. No full block was mined; these are divisions of the limits by one transaction.

Where that script cost goes, measured by swapping the hash out in the harness: hashing is 6.9% of the n=32 w=16 cost (2,692 of 38,890; the chain hashes match the interpreter’s cost table within one unit) and 9.4% at w=256. The other 93% is not hashing; how it splits among slices, folds, lambda dispatch and serialization was not measured.

4. A page that does it, on the public testnet

oneshot (cafebedouin.github.io/skunkyard/oneshot, source in skunks/oneshot/) is a static page with no framework and no server: generate or restore a seed, show the address, fund it from any wallet, spend the whole box once to an address you name, testnet only. The WOTS lock needs no prover: Fleet builds the transaction and a Blake2b library signs it. The P2SH lock hides a proveDlog key behind a 24-byte hash until the spend is broadcast; it is not a post-quantum lock, and its protection ends in the mempool. It needs the reference interpreter, so the page loads sigmastate-js (7 MB, 1 MB gzipped, only for a P2SH spend; 0.6 to 2.5 s to load, then about 75 ms to reduce and sign the first time). A TypeScript signer reproduces the Scala harness byte for byte (258 checks), Fleet’s output bytes equal the reference serializer’s (156 checks), and a CI workflow for those checks is included and runs once the repository is on GitHub. Five testnet spends, two WOTS and three P2SH, each submitted first with a corrupted signature or proof and rejected by the script check, then valid and confirmed; two of them were the built page driven in a DOM emulator; full ids and rejection texts in skunks/oneshot/RESULT.md. Not yet done: a person clicking through the page in a real browser, and a submit path from the hosted HTTPS page, which cannot call an HTTP node except on localhost (not tested in a browser).

What building it turned up. The reference interpreter changed the context variable its P2SH script reads from 1 (sigma-state 5.x) to 126 (6.x), and the SDKs did not follow: for one P2SH address, sigmastate-js writes the var-126 box script, Fleet and the explorer the var-1 script, and sigma-rust (ergo-lib-wasm 0.28.0) a var-1 script without the OptionGet step. On a 6.0.6 devnet node the first two spend and the sigma-rust form is rejected (java.lang.ClassCastException: class scala.Some cannot be cast to class sigma.Coll), so a box that a sigma-rust-based payer writes for a P2SH address cannot be spent, and ergo-lib-wasm cannot parse back the tree it wrote. Fleet’s toP2SH() hashes the whole tree rather than the proposition, which yields an address whose boxes the reference interpreter cannot reduce (shown in sigmastate-js, not on a node). The explorer files a var-126 box under a P2S address, so an address lookup misses it. A wallet must therefore read the funded box’s script rather than derive it from the address. Filed, with reproductions: sigma-rust #928 with the fix in PR #929, and Fleet #219.

5. What the options look like

  • No fork, now. P2SH already works at the node: on 6.0.6 Fleet’s var-1 box spends, the reference’s var-126 box spends, and sigma-rust’s box does not; Nautilus has no P2SH spending path by a code search (a weak zero). It hides a key at rest until first use and only if the address is used once. A WOTS lock works as shown. A hybrid sigmaProp(wotsOk) && proveDlog(pk) is expressible and unmeasured; a boolean check cannot be a hidden OR branch of a sigma proposition.
  • One soft fork, as a cost floor. A boolean verifyWots-style opcode in the shape of EIP-0045’s verifyStark, with domain-separated hashing specified from the start. If a native check cost only the measured 2,692 hashing units, the node total would be about 15,700 and a block would hold about 510 such spends, against 596 P2PK; the 2,144-byte signature stays in the transaction, so the size bound stays about 542. That price is a guess, and a boolean opcode does not give sigma-protocol composability.
  • Many-time keys. A tree of WOTS keys committed as an AvlTree digest in a register, with the next-leaf index carried in the box and enforced by the script. Unimplemented.
  • Votes. Block cost and block size are miner-voted parameters, about 1% per epoch, with a code ceiling of Int.MaxValue / 2; that step is a mechanism, not a proposal.
  • Legacy coins. Needs its own thread, after a census at the tip. This run cannot describe dormant coins.

6. Asks

  1. Run the scan on a synced node and post the by-category table, the protocol rows, the height, the check lines and the commit.
  2. Maintainers: two questions. Is a native hash-based verifier opcode something you would consider, and what would you want measured first? And which box script is canonical for a P2SH address now that 6.x reads variable 126 while the SDKs and the explorer write variable 1?
  3. SDK and wallet developers: reconcile the P2SH box script across Fleet, sigma-rust and sigma-state 6.x (sigma-rust #928 with PR #929; Fleet #219), and have wallets read a funded box’s script rather than derive it from the address. A reduction path through the reference interpreter’s JavaScript build (sigmastate-js), as the explorer already uses as a fallback, is one way to stop a wallet lagging the node; the page is a reference flow for it.
  4. Break it. Testnet box 894e17301351c1d6645342cb4a78c256c848c9ebea27089db7b7c791a38b74a7 holds 1 ERG under the WOTS lock at address 517F9i5j… (full address in skunks/oneshot/RESULT.md). Spend it without the seed, or forge against q2/wots-constant.es in the harness, or find a message the binding misses. A working forgery is the most useful reply this thread can get.
  5. Corrections. Each measured claim names its script and its printed output; if one does not reproduce, that is a finding.

Repository: github.com/cafebedouin/skunkyard: SCOPE.md, LITERATURE.md, q1/ (scan), q2/ (verifiers, harness, devnet scenarios, hash share), skunks/oneshot/ (page, signer, testnet runs), notes/.

Two clarifications, both derivable from the post’s own tables:

  • The summary’s capacity line, about 154 WOTS spends against about 596 P2PK per block, is the cost bound for a single-input spend. The size bound in section 3’s table is 542 against 4,367, and a native opcode priced at the hashing units does not move the size bound, since the signature stays in the transaction.
  • Storage rent falls on a box unspent for 1,051,200 blocks, at 1,250,000 nanoERG per byte per period at the current factor: about 1.05 ERG per period for a WOTS box of 840 bytes of script, against about 0.13 ERG for a P2PK box. “Nothing for holders to do today” is about action; a lock that sits for four years pays for its bytes. (The one-time box pays this only if it is never spent; the many-time box of the follow-up resets the clock on every spend.)

Prepared with Claude Code (Anthropic, Claude Fable 5.1; review seats on Claude Opus 5.5, Grok and Gemini) for cafebedouin using peeryard v0.1.0 (ba56b25) on ergo 6.0.6 (devnet, one mining node), a public ergo 6.0.1 testnet node, sigma-state 6.0.7 for the driver, Fleet for the testnet funding, and a scan of a stopped mainnet node’s history store; reviewed in five rounds before posting (posts/REVIEW-manytime-reply.md).

A hash-based lock (WOTS, n = 32, w = 16) that one key set spends many times, with the chain refusing any leaf below the key set’s index for every box the key set owns, runs on today’s node with no fork: on a devnet and on public testnet, as a prototype, not something to use yet. Many-time keys, which the post’s section 5 listed as unimplemented, with the index in a box rather than the digest in a register as that sketch had it. Details, numbers, limits and one “break it” box follow; nothing is asked of holders or of maintainers.

The construction. Two scripts around a state box marked by a token of supply 1, the shape Ergo contracts already use for a state box. The singleton is one box per key set; its script commits to 16 WOTS keys as the AVL digest of their commitments (33 bytes at any count; the count is a script constant too) and carries the key set’s index in R4. A spend names a leaf at or above the index, supplies that leaf’s signature and lookup proof, and must recreate the singleton with the token and an index above the leaf it used and below the leaf count; on the last leaf the token must instead leave to a script that is neither this one nor the deposit script, at index 0, the intended place being the next key set’s singleton. The deposit script, 166 bytes: a box here is spendable only in a transaction whose first output carries the token and has either advanced the index or changed the script against the token-carrying input, and only if that input’s value exceeds the largest storage-rent fee a miner vote can set for its size; together that is to say a transaction in which the singleton’s script ran (next paragraph). The deposit address is the one a holder gives out; a plain payment to it is a usable deposit, and because the script depends only on the token, the address is the same for every key set that inherits the token (run, not only read: the second key set’s deposit address printed identical). The signed message covers every input id, every data-input id (by reading; no data-input case ran) and every output. The verifier is section 2’s, reading the commitment from the tree; the index and token checks sit on the left of the && and fail before any hashing.

Why the deposit rule is phrased that way. Storage rent lets anyone spend a box that has sat for 1,051,200 blocks by recreating it minus the fee, same script, registers and tokens, and the box’s script does not run on that spend (node rule, read, not run). A deposit rule that asked only “is the token among the inputs”, which an earlier draft had, would have let a rent collector attach every deposit to a four-year-old singleton’s rent spend. Rent collection of a singleton whose value covers the fee can neither advance the index nor change the script, so the rule above holds only when the state script ran. A singleton worth less than its fee would be different: rent consumes it whole with no constraint on the outputs, the collector would take the token under a script of their own, and that would satisfy the “script changed” branch, so every deposit present and future would be theirs (by reading of the node’s expired-box check; a fifth review round caught an earlier draft calling this stranding). So the deposit rule also requires the singleton’s value to exceed the largest fee a vote can set, 2,500,000 nanoERG per byte, about 3.33 ERG at its size (derived); every rent spend of a singleton that deposits could follow is then a covered one, which recreates it unchanged, and a singleton that falls below the floor makes its deposits unsweepable until the wallet tops it up. Found by review seats by reading; it cannot be shown on testnet for about a million blocks.

Executed, on public testnet (through a public testnet node, ergo 6.0.1, block version 4; the project’s testnet wallet minted the token, then in one transaction funded the singleton, three plain deposits and one plain box at the singleton’s own address). Rejected by the node, with script costs in units: a forged signature, 37,611 (the full verification, no fee); a spend that does not advance the index, carrying a deposit, 75 (and, evaluated locally, the deposit’s own script refuses it at 77); a deposit spent without the singleton, 21; a transaction signed over the singleton alone and posted with a deposit added, 37,987; the plain box at the singleton’s address, which has no token, with a valid signature, 23; leaf 0 signed again after the index read 1, 46 (a false from the index check, not an exception); the continuing index written as the leaf count, 84; the last leaf with the token kept under the same script, 67; the last leaf with the token sent to the deposit script, 66. Confirmed: leaf 0 sweeping the first deposit (transaction 9a561b2f…, index to 1); leaf 3 sweeping the second (bd58119c…, index to 4); leaf 15 moving the token to a second key set’s singleton at index 0 (de6bf922…); that key set’s leaf 0 sweeping the third deposit, which had been funded before the rotation (66963ab8…); the token carried every time, every swept deposit gone from the UTXO set. The devnet (ergo 6.0.6, block version 4) gave the same thirteen verdicts with the same rejection costs except the two that include a hash count.

The hybrid. proveDlog(owner) && (this lock) ran only under the earlier per-box versions, on the devnet: the hash-only spend without the curve proof rejected, both proofs confirmed, about 200 to 500 units over the plain form in matched rounds (derived) and 95 bytes (derived: 56 of proof, 39 of the larger box). Not rerun under this version.

Numbers, per spend with one deposit (derived from the printed figures). Script cost 37,794 to 38,064 units on testnet and 37,732 to 38,200 on the devnet for the sweeps, of which the deposit’s script is 77; the width is message-dependent and the many-time machinery is not separable from it; the one-time box in section 2 measured 37,592. 3,871 bytes against 2,345: the recreated singleton, 1,331 bytes with its 1,253-byte tree, is an output of every spend, and the deposit is a second input. Per mainnet block about 147 by cost (8,001,091 ÷ (38,200 + 15,708 for two inputs and three outputs + 400, the devnet’s measured charge above that formula on both input shapes, by inference four token accesses at 100)) and 328 by size (1,271,009 ÷ 3,871), against 158 (8,001,091 ÷ (37,592 + 13,003)) and 542 for the one-time box on the same measured basis; section 3’s 154 used the harness worst case.

Rent. The singleton persists, so it owes about 1.66 ERG per four years at the current factor (derived: 1,331 bytes × 1,250,000 nanoERG), charged only if it sits unspent that long; its clock restarts whenever the wallet sets the recreated box’s creation height to the current one, as this driver does. A deposit is 208 bytes and owes rent on the same terms (about 0.26 ERG per four years, derived; one worth less is consumed), so dust left unswept for four years is lost.

What the chain enforces (for the key set, while the token sits in a singleton of a root never used before; executed unless marked). The index never moves backwards and cannot be written past the tree; a leaf below the index cannot spend; a deposit moves only in a transaction where the singleton’s script ran and advanced the index or rotated (the rent exclusion by reading). So a wallet restored from an old backup that signs a leaf the index has passed gets a rejection, for the singleton as run, for a deposit by reading (any failing input fails the transaction). The property this adds over signer-side state, by reading: once a spend with leaf k has confirmed deeply enough to be final, every signature of leaf k is useless, so the forgery window of a doubly signed leaf closes there.

What it does not enforce. The chain cannot see a signature that never reaches it. A signer who signs any leaf twice over distinct messages has opened the window: with two signatures a forger can sign some spends of their own at that index, which on an ordinary leaf means the deposits present and on the last leaf means the token as well, and so every future deposit (WOTS property, by reading, not tested). The test procedure itself did this: at least two valid leaf-0 signatures and two valid leaf-15 signatures went to the public node before the corresponding spends confirmed, a race that stayed open until then, in which no competing spend confirmed; a wallet must never do what the test did. A replacement signed with leaf k + 1 while the original with leaf k then confirms leaves k + 1 disclosed with the index at k + 1, and a wallet rebuilt from the chain that signs k + 1 again has opened the window too.

What the wallet must do. Its counter moves at signing and never decrements; it never pre-signs a child against the predicted id of an unconfirmed singleton, because the transaction id covers context extensions, which a relay can pad, so the id can change under a valid spend (by reading); rebuilt from the chain, it would skip ahead past the index by a margin, which the “at or above” rule allows at the cost of the skipped leaves (a rule, not something this driver implements). The token is the key to every deposit: mint exactly one (the supply is public in the mint transaction); keep the singleton above its value floor (the script refuses sweeps otherwise) and refresh it within four years; at the last leaf send the token only to a singleton with the same token id, a root never used before and index 0, since the script forbids only two byte strings (a re-encoded deposit script or an always-true script is as bad), and a token returned to a root already used would reopen that root’s leaves; never pay the singleton’s own address, which is visible on chain, because a payment there without the token is lost; never let a non-Int reach the singleton’s R4 (an EIP-4 mint straight into the singleton would; this run minted to a plain box first); set the recreated singleton’s creation height to the current height, which the script does not check; build every spend with the singleton as the first output, which the scripts do check. A depositor who wants the guarantee checks the token’s mint transaction for supply 1, since the deposit address does not show it. Restoring needs the token id, since the singleton’s address changes at every rotation while the deposit address does not; the deposit address is a long P2S address, which some services may refuse as a withdrawal target.

Not shown. Hiding which leaf signed (the register and the proof name it, so the usage count is public and the token id links key sets across rotations; the proof’s sibling hashes reveal nothing about unused leaves’ secrets beyond the hash’s one-wayness, by reading); domain separation in the chain hashes, absent here as in section 2; adding leaves (the tree is built off chain and read-only); heights above 4, several deposits in one spend, a second rotation, a data-input case, the hybrid under this version (not run); a bad lookup proof at a legal index (by reading, an exception rather than a false); a forged spend still costs the node the full verification and pays no fee, as in the post.

Break it. On testnet, key set B’s singleton holds the token at index 1 with about 4.1 ERG, above its floor, and a 0.3 ERG deposit sits at the deposit address:

singleton  e55b03bbfc7466785e0b5f58c7f5a4e587b7754a98beafec089366e06a6910d9
token      f3485248dfb91f1a4074d88e5815da041c8b4724af77c8668a9adf120b4f833c
deposit    7627ad1fcd14959907cb26b22dde99355ff3bc79d4e86ca3abf83bd04e101354
address    dnav7UZh5pNnEvVCHB9uqaWM8hHERvCHNofCbufsK5QXvhcPojBuueQaoqY2R4RMDCMSStwhHeKX4WNDAYRakfQCAkrw9GEhEa7w7zP4ezdXmihev9Vpcr7PWoBeqGkUaXEyhsqW4fRxSWCaxLQ5guZgbmb7u5Nqztm1kAyAL5UHjqgQStH2XX7myk9FzVcsjCgMXDDkx3ftbsPAoMwegDLraxeLaQ8wqVMpmhnra

Every transaction above is in the repository’s result file. A break is a confirmed testnet spend of that deposit, or a transaction the two scripts accept on a devnet or in the harness, carrying no valid signature of an unused leaf of key set B; or an argument that a sentence under “What the chain enforces” is false, which is how the rent finding above was made.

Prior work. QRL has enforced one-time use of XMSS indices at consensus since its XMSS addresses (2018 by its documentation): a per-address bitfield over the first 8,192 indices, usable in any order, and above that an ascending counter; this singleton is that counter in UTXO form, with every leaf under the counter, so a skipped leaf is burned. WOTS-Tree (Mateos, ePrint 2026/374) brings XMSS to Bitcoin with up to 2^21 leaves and recovers the index by scanning the chain, the signer enforcing it; Kudinov and Nick (2025/2203) prefer stateless schemes for Bitcoin because of the state-rollback failure, while keeping the stateful option open; BPQS (2018/658) chains two-leaf trees with signer-side state. On account chains, the Solana Winternitz vault (2025) has a program verify one WOTS signature and move the funds to a vault under the next key, and Ethereum contract-wallet proposals keep a stable address while rotating a stored key hash (both from a web search, neither source read in full). What is added here is a many-time counter enforced by a box script on a UTXO chain under the stock interpreter. The search scope is in the repository’s prior-art note; it did not cover the Bitcoin mailing lists or IOTA, nor Ergo’s own forum and chats for stateful-counter contracts.

How many leaves. Under the singleton one signature sweeps any number of deposits within the block limits (by reading; one per spend ran), so leaves consumed are transactions signed, plus replacements, skip-ahead margins and one refresh per four years. Over 791,286 mainnet blocks (789,437 contiguous from genesis plus 1,849 near the tip; by height the 2019 to 2022 era, 42% of today’s height, derived), 263,160 P2PK keys signed at least one transaction: median 2, 47.2% exactly once, 99.7% at most 1,024 (derived), 776 keys above 1,024 (0.29%, derived; who they are is not measured) with 27.4% of all signings between them (derived), the largest at 87,909, below 2^17. A 60-block sample of the later era through the explorer, reweighted for the block sample’s bias toward frequent signers, puts the median active address at 24 lifetime transactions (an upper bound on signings, since receipts count) and 1.9% above 1,024, over 263 addresses with a count; a second draw of 40 blocks gave 18 and 2.0%; a few hundred addresses is a small sample. So a height of 10 would cover nearly every key of the first era, height 17 every key in it, the later era runs heavier, and rotation at the last leaf, which this run exercised once, covers the rest. Repository q3/, aggregates only; the scanner runs on a stopped node’s history store in one command, and anyone with a synced node can get the exact later-era figures.

Scripts, driver, hooks, rig logs and the testnet artifacts: skunkyard/skunks/manytime at main · cafebedouin/skunkyard · GitHub