Protocol v1, on Robinhood Chain

Signatures that survive Q-day.

On Q-day every secp256k1 signature on this chain becomes deniable. This page is how Hashborn derives an identity from one wallet signature, signs every action with a one-time hash-based key, anchors it on Robinhood Chain, and lets anyone check it with SHA-256 alone.

01

Overview

A visitor derives one identity: a Merkle tree of 256 one-time WOTS keys over SHA-256, built from a single personal_sign of the connected wallet. The tree root and a public seed are the public key. Every statement the visitor makes afterwards is a 32-byte digest signed by the next unused leaf.

  • Agents are registered under the identity and every action they take is attested by a burned leaf.
  • Agent wallets are secp256k1 keys derived from the identity; every transfer carries a second, hash-based signature for the record.
  • RWA manifests are Merkle roots over document hashes, signed by a leaf and anchored on chain.
  • Liquidity vaults only release funds when a WOTS signature verifies on chain, through the SHA-256 precompile.

The only cryptographic assumption anywhere is that SHA-256 is second-preimage resistant. No curve, no lattice, no trusted server. The Verify page replays every step in your browser.

02

Threat model

Q-day is the day a quantum computer runs Shor's algorithm at scale. On that day every exposed secp256k1 public key yields its private key. On an EVM chain a wallet's public key is exposed by its first outgoing transaction, so after Q-day an attacker can sign as any wallet that has ever sent one.

What this protocol protects

  • Provenance: which identity registered an agent, published a manifest, authorised a withdrawal, and when.
  • Records: a transfer or action attested by a leaf cannot be forged by anyone who later recovers the wallet key.
  • Vault funds: the vault contract verifies the hash-based signature itself, so the wallet key alone cannot spend.

What it does not protect

  • The chain still enforces secp256k1 for ordinary transactions. An agent wallet's balance outside a vault is as safe as the curve. The dual signature makes the record survivable, not the balance.
  • An identity derived from the wallet signature alone can be re-derived by anyone who holds the wallet key and signs the derivation message. A passphrase closes this: the attacker then also needs the passphrase, stretched with scrypt.
  • A site that tricks you into signing the derivation text learns your identity seed. The text names the product, the chain and the purpose so it is recognisable, and the identity page shows it in full before you sign.

Trust boundaries

  • Wallet to browser: one signature, read once, kept only in tab memory.
  • Browser to chain: public values only (root, public seed, digests, signatures, auth paths).
  • Link to Verify page: untrusted JSON, parsed against a strict schema and capped in size before any hashing.

03

Primitives

Hash: SHA-256, n = 32 bytes. Every chain step, leaf and node is hashed with its own address, so no two hash calls in a tree share an input prefix.

F(pubSeed, ADRS, x) = SHA-256(pubSeed || ADRS || x)

ADRS is 32 bytes: four big-endian u32 fields, then 16 zero bytes.

fieldbytesmeaning
type0..30 chain step · 1 leaf public key · 2 tree node · 3 secret key PRF
a4..7leaf index (chain, leaf, PRF) or node height (node)
b8..11chain index (chain, PRF) or node index (node)
c12..15chain step (chain only), 0 otherwise
pad16..31zero

04

WOTS

Winternitz one-time signatures with w = 16. A 32-byte digest becomes 64 base-16 digits d0..d63. The checksum C = Σ (15 − di), at most 960, is written as 3 base-16 digits, most significant first, for 67 digits in all.

sk_i    = SHA-256(skSeed || ADRS(3, leaf, i))     secret start of chain i
pk_i    = F^15(sk_i)                              chain end, step addresses 0..14
sigma_i = F^{d_i}(sk_i)                           signature value, steps 0..d_i-1
verify:   F^{15-d_i}(sigma_i) == pk_i             steps d_i..14

Signing walks Σ di steps and verifying walks the remaining Σ (15 − di). Together that is exactly 67 × 15 = 1,005 hash calls for any digest. The verifier alone makes between 45 and 990 calls. The checksum stops an attacker from lowering any digit without raising another, which is what makes a one-time key one-time.

05

Merkle identities

One identity is 256 WOTS keys under a Merkle tree of height 8. The verifier never sees a WOTS public key directly; it recomputes the chain ends, hashes them into the leaf, and climbs the auth path.

l_i     = SHA-256(pubSeed || ADRS(1, i) || pk_i,0 || ... || pk_i,66)
n_h,j   = SHA-256(pubSeed || ADRS(2, h, j) || left || right)    h = 1..8
root    = n_8,0
public key  = root || pubSeed                                   64 bytes
signature   = leaf u32 || sigma_0..66 || auth_0..7              2,404 bytes
pq address  = "pq1" + base58(SHA-256("pqc-rh" || pubSeed || root)[0..20])

The auth path is the sibling at each of the eight levels, leaf level first. A leaf index is used once. The browser refuses to sign twice with the same leaf in one session; the registry is the source of truth across sessions.

06

Derivation

  1. The wallet signs the fixed derivation text with personal_sign. The text names the protocol (pqc-rh), Robinhood Chain and the purpose, and ends with the lower-case wallet address. It carries no nonce.
  2. The wallet is asked a second time. Equal signatures mean a deterministic signer (RFC 6979), and the signature is the key material. Different signatures mean the signature cannot be the key: a passphrase becomes mandatory and the signature is left out of the key material.
  3. Optional passphrase: h_pass = scrypt(NFKC(passphrase), salt, N = 2^15, r = 8, p = 1, 32 bytes) with salt = SHA-256("pqc-rh/identity/passphrase-salt/v1" || "\n" || wallet). A passkey can stand in for it: its WebAuthn PRF output for SHA-256("pqc-rh/identity/passkey/v1\n" || wallet), written as "pqc-rh/passkey/v1:" || hex, is the passphrase, so nothing below changes.
  4. IKM = sig || h_pass (deterministic signer) or IKM = h_pass (non-deterministic signer).
  5. skSeed = HKDF-SHA256(IKM, salt "pqc-rh/identity/v1", info "sk:" || wallet, 32) and pubSeed = HKDF-SHA256(IKM, same salt, info "pub:" || wallet, 32).
  6. The tree: 256 leaves × (67 PRF calls + 67 × 15 chain steps + 1 leaf hash) + 255 nodes = 274,943 SHA-256 calls, in a Web Worker, under a second.

The seed and the tree live only in the tab. Lock, a wallet switch, a disconnect or closing the tab forgets them. No secret is written to storage and nothing is logged; the browser keeps only the list of leaf indices it has signed with, so a reload never hands out a leaf twice.

07

Registration

One statement about the public key, signed twice: the wallet signs the statement text with personal_sign, and leaf 0 signs digest("pqc-rh/identity/v1", {height, pqAddress, pubSeed, root, wallet}). Leaf 0 is therefore always burned at derivation and the first attestation uses leaf 1.

pqc-rh identity registration, protocol v1
pq address: pq1...
root: 0x...
public seed: 0x...
height: 8
wallet: 0x...

08

On-chain anchor

PQRegistry.register(root, pubSeed, wots, auth), sent by the wallet. The registry verifies the leaf 0 signature over the registration digest on chain and binds the root to that wallet, one identity per wallet. The emitted event is the anchor: a public timestamp, from before any break, that this wallet committed to this root.

Agents, manifests and transfer records are published by the same wallet with anchor(domain, record, signature): the canonical JSON the leaf signed and the 2,404-byte signature. The registry computes the digest from those bytes, verifies the signature on chain through the SHA-256 precompile, and only then burns the leaf the signature names and emits both in its event, so a record and its proof can be rebuilt from the chain alone, without this site. Because nothing is anchored without a valid signature, a wallet key on its own, stolen today or forged after Q-day, cannot burn a single leaf.

The registry keeps a 256-bit map of used leaves per wallet and refuses a leaf twice, whether an anchor or a vault withdrawal used it. It refuses the two domains that are verified elsewhere, the registration and the vault withdrawal, so no record can stand in for either, and a domain with a line break. An attestation's txHash is null until the anchoring transaction is confirmed.

09

Attestation domains

Every digest is SHA-256(domain || "\n" || canonical JSON). Canonical JSON is one object with keys sorted by code unit, no whitespace, and values limited to strings, finite numbers, booleans and null. Amounts are strings. A nested object or array is rejected so two encoders can never disagree.

domainpillarfields (sorted)
pqc-rh/identity/v1Identityheight, pqAddress, pubSeed, root, wallet
pqc-rh/proof/v1Identityleaf, nonce, wallet
pqc-rh/agent/v1Agentsname, operator, policyHash, scheme
pqc-rh/agent-action/v1Agentsagent (the registration record id), at, kind, summary
pqc-rh/transfer/v1Walletsamount, asset, at, index, to, txHash, wallet
pqc-rh/rwa/v1RWAasset, at, kind, manifestRoot, summary; a new asset adds category, custodian, documents, jurisdiction, name, token, valuationUsd
pqc-rh/vault-withdraw/v2Vaultspacked, in call order: chainId, vault contract, vaultId, index, to, asset, amount, deadline

The one exception is pqc-rh/vault-withdraw/v2, which the vault contract recomputes itself: SHA-256(abi.encodePacked("pqc-rh/vault-withdraw/v2\n", chainId, vault, vaultId, index, to, asset, amount, deadline)), 193 bytes, so the signature binds the exact transfer the contract executes and the last moment it may run.

A domain is never reused for a different field set; a change adds a new domain with the next version suffix.

10

Agents

An agent is registered by its operator's identity: the digest of its name, operator, policy document hash and chosen scheme, signed by one leaf and anchored. From then on each action the agent takes is a new attestation under pqc-rh/agent-action/v1 that names the agent by the id of its registration record, so an auditor can walk the leaf sequence and check that every action was authorised by the key that registered the agent, and one identity can run several agents without their logs mixing.

  • An agent has 256 attestations before its identity is exhausted.
  • Rotation: a fresh identity is derived (new passphrase or new wallet) and its registration digest is signed by a leaf of the old one, chaining old to new. An agent in that window shows as rotating; one whose leaves are gone without a rotation shows as retired.
  • The policy hash binds the agent to the exact document it was registered with. A changed policy is a new registration.

11

Agent wallets

Wallet index is the secp256k1 key HKDF-SHA256(skSeed, salt "pqc-rh/wallet/v1", info u32be(index), 32). The key never leaves the identity object; the page sees an address and a sign function.

A transfer is signed twice. The wallet's own key signs and sends the transaction itself, which is what the chain enforces and what moves the funds. Once it is confirmed, a WOTS leaf signs digest("pqc-rh/transfer/v1", ...) over the wallet, its index, the recipient, the asset, the amount and that transaction's hash, and the registered wallet anchors the record. The second signature is what remains checkable after the curve falls: that this identity, at this leaf, authorised this exact transaction.

12

RWA manifests

Documents are hashed in the browser with SHA-256 and never uploaded. The manifest root is a binary Merkle tree over those hashes in the given order, with node = SHA-256(0x01 || left || right) and an odd node paired with itself. The root is always a node, so a manifest root can never equal a single document hash. One document gives SHA-256(0x01 || h || h).

The proof attestation signs the asset's category, custodian, issuer, jurisdiction, manifest root, name and valuation under pqc-rh/rwa/v1. A revaluation or a new document set is a new attestation with the next leaf; a revocation is an attestation that names the root it revokes.

13

Liquidity vaults

A vault is a PQVault position bound at creation to its owner's identity root. Creating it signs nothing and burns no leaf. Anyone can deposit ETH or the vault's token. A withdrawal is the only operation that needs the hash-based key, and the contract verifies it itself through the SHA-256 precompile at address 0x02.

The owner sets two limits at creation, and the contract bounds both: each withdrawal may take between 10% and 100% of the balance, and withdrawals may be spaced by up to 30 days. The floor exists because every withdrawal uses one of the identity's leaves: at a smaller cap, the leaves would run out before the vault could be emptied.

  1. The app computes the packed digest over the chain id, the vault contract, the vault number, its current index, the recipient, the asset, the amount and a deadline an hour ahead, and signs it with the owner's next unused leaf.
  2. One transaction carries the leaf index, the 2,144 bytes of chain values and the eight auth nodes, 2,404 bytes in all. EVM calldata has no staging limit.
  3. The contract checks the deadline, the cap and the cooldown, recomputes the digest from the call's own arguments, finishes all 67 chains (45 to 990 precompile calls), hashes the leaf, climbs eight levels and compares the root.
  4. On a match it burns the leaf in the registry, pays amount to to and moves the vault to index i + 1: vault[i] is spent and vault[i + 1] holds the remainder.

Each vault index accepts exactly one withdrawal, because the digest binds the index, and each withdrawal burns one leaf. A signed withdrawal that was never sent, or failed, cannot move funds after its deadline. An identity authorises 256 statements across every product before it rotates. A vault whose owner has spent every leaf can never pay out, so it takes no more deposits. The lineage from vault[0] onward is public.

Facts about the contracts

  • The registry and the vault have no owner, no pause and no upgrade path. Nobody, including this site, can move a vault's funds, change its limits or stop it.
  • For the same reason a fault found after deployment cannot be patched: a fix is a new deployment, and vaults in the old contract keep the old rules.
  • USDG is issued by a third party that can pause the token and freeze addresses. If it froze the vault contract, every USDG vault would stop paying out until it lifted the freeze.
  • A token that charges a fee on transfer is credited with what actually arrived. A token whose balances shrink on their own is not supported.

14

Signature schemes

WOTS + Merkle is the root of trust and burns one leaf per attestation; it is the scheme this build signs with, and the only one the registry accepts. Many-time NIST schemes can be certified once by a leaf and then sign without burning more; they are listed for later, when they have an anchor of their own.

schemestandardassumptionsignaturepublic keystatus
WOTS + MerkleRFC 8391 styleSHA-256 second preimage2,404 B64 Bavailable
ML-DSA-65FIPS 204Module-LWE / SIS lattices3,309 B1,952 Blater
SLH-DSA-128sFIPS 205SHA-256 (stateless)7,856 B32 Blater
Falcon-512FIPS 206 (draft)NTRU lattices666 B897 Blater
ML-DSA-87FIPS 204Module-LWE / SIS lattices4,627 B2,592 Blater
SLH-DSA-SHAKE-128fFIPS 205SHA-3 / SHAKE256 (stateless)17,088 B32 Blater
Falcon-1024FIPS 206 (draft)NTRU lattices1,280 B1,793 Blater
secp256k1 + ML-DSA-65IETF composite drafteither curve or lattice must hold3,374 B1,985 Blater

15

Security analysis

  • Forging a signature for a registered root needs a second preimage somewhere along a chain, a leaf or a node: about 2256 classical work, about 2128 with Grover. No known quantum speed-up does better against SHA-256.
  • Reuse of a one-time key leaks part of its secret. The browser never hands out a leaf that has signed, even after a reload or a failed transaction, and the registry refuses a leaf index that wallet has already used.
  • Without a passphrase the identity is exactly as strong as the wallet's secrecy today and no stronger than the curve after Q-day. With a passphrase an attacker who recovers the wallet key still needs the passphrase through scrypt at N = 215.
  • A non-deterministic signer is detected by signing twice; its signature is excluded from the key material rather than producing an unrecoverable identity.
  • No server holds secrets or can forge: every check on these pages runs from public values with SHA-256. The registry verifies every signature it anchors and can only refuse.
  • Secrets live in a closure in tab memory, zeroed on lock. So a reload does not ask your wallet again, the tab keeps them sealed with AES-GCM under a key the page can use but never read, for at most 12 hours; Lock, switching wallets or closing the tab forgets them. A passphrase string cannot be zeroed by JavaScript and is left to the garbage collector.

16

Parameters

parametervalue
HashSHA-256
n32 bytes
w16
Chains67 (64 message + 3 checksum)
Chain steps15
Tree height8
Leaves per identity256
Signature2,404 B
Public key64 B
Sign + verify1,005 chain hashes
Tree build274,943 SHA-256 calls
scryptN = 2^15, r = 8, p = 1, 32 bytes
HKDF saltpqc-rh/identity/v1
Wallet saltpqc-rh/wallet/v1
Addresspq1 + base58 of 20 bytes

17

Verify it yourself

The same two functions the Verify page uses. Recompute the digest from the fields, then verify the one-time signature against the root. Every value involved is public.

import {pq} from '@/lib/pq'
import {DOMAINS, type Attestation} from '@/lib/types'

// 1. Recompute the digest from the fields the attestation claims to cover.
const digest = pq.digest(DOMAINS.agentAction, {
  agent: 'pq1cBjh2zgQQ7FDKmgo9etZmevZPn4',
  at: '2026-10-09T06:00:00.000Z',
  kind: 'attest',
  summary: 'Rebalanced the ETH/USDG pool on vault 2',
})

// 2. Verify the one-time signature against the registered root.
declare const attestation: Attestation
const result = pq.verify(digest, attestation.signature, {
  root: attestation.root,
  pubSeed: attestation.pubSeed,
  height: 8,
})

console.log(result.valid, result.hashCalls, result.trace.length) // true, 45..990, 12

Without this code: split the digest into 67 digits, finish each chain with F, hash the chain ends into the leaf, climb the eight auth nodes and compare. Anything that does SHA-256 can do it.

18

For developers

A public API over the same data, open to any site. It reads the chain and the index and checks every record with SHA-256 before it answers; anything it says can be recomputed from the chain without it.

callreturns
GET /api/v1/records/<id>One anchored record: its signed fields, the record text and the 2,404-byte signature as published, signer, anchoring transaction, and whether it verifies. Cached for a day.
POST /api/v1/verifyVerifies an attestation sent as JSON (the format the Verify page takes): signature, leaf, pq address.
GET /api/v1/identities/<wallet>The identity a wallet registered on chain: root, public seed, pq address, leaves used and left.
GET /api/v1/badge/<id>An SVG badge that reads verified, does not verify, or not found.

A record id is the anchoring transaction hash followed by the event's position in it, as in the address of every record page. Put the badge next to whatever the record proves:

<a href="https://hashborn.com/records/<id>">
  <img src="https://hashborn.com/api/v1/badge/<id>" alt="Hashborn: verified" height="20">
</a>