Skip to content

Audit the receipts

NexusPool signs every record it writes to its own log. The signing key's public half is published, the byte layout that gets signed is published, and the records are served over a plain HTTP endpoint. That is enough for anyone to check our claims with their own code, and this page is the whole procedure.

What a receipt proves, and what it does not. A valid signature proves the record came from the holder of the published key and that not one byte has changed since. It does not prove the pool wrote down everything it should have. Signing makes us accountable for what we publish; it cannot make us accountable for silence. Read the receipts as a commitment we cannot later deny, not as an audit of work you never saw.

1. Get the key

One request, no authentication. The document carries a key per chain, the domain tag, and the field order of the signed bytes.

curl https://nexuspool.io/.well-known/nexuspool-custody.json

The keys it serves right now, as x-only 32-byte public keys in hex:

chainauthority public key
btc124797e8bc0ec89ed60b77c30e1770c624407f72659dc97aa4c4fd7d9377106c
bch39e0a44418a2f0d831fc9166ffd4f1373bb6f263d753ed6d08862da8ed4a1d46
ltc450a714033c4b01731820bca8c32f00f3eb72c56c14b3c8b43cb0a7d0f357387

Pin the key you read here. If it ever changes without us saying so in the changelog, treat every receipt signed under the new one as unverified until you know why.

2. Rebuild the signed bytes

A signature is over a 32-byte digest, not over the record. Concatenate these six fields in this order, then take a single SHA-256 of the result. There is no length prefix on the whole message and no trailing padding.

fieldencoding
domainutf8, no terminator
sequint64 little-endian
type_lenuint8
typeutf8
payload_lenuint16 little-endian
payloadutf8, unescaped, printable ASCII only

The domain is the fixed ASCII string NexusPool/Custody/v1, 20 bytes, with no terminator. It exists so a signature over one kind of record can never be replayed as a valid signature over another.

Two details decide whether your implementation matches ours. The two length fields are little-endian, like seq. And the payload you hash is the unescaped string, the one your JSON parser hands back, never the raw text between the quotes in the response. A payload containing a backslash will verify under one reading and fail under the other.

3. Verify

The signature is 64 bytes, BIP-340 Schnorr over secp256k1, exactly as Taproot uses it. Any library that verifies a Taproot signature verifies these. Pass it the 32-byte digest from step 2 and the x-only public key from step 1.

  1. digest = SHA256(preimage)
  2. valid = schnorr_verify(sig64, digest, authority_xonly32)

4. Fetch receipts

Receipts are served by address. The literal address sample returns a pool-wide page so you can test your verifier before pointing it at your own work.

curl 'https://nexuspool.io/api/custody-receipts?addr=sample'
curl 'https://nexuspool.io/api/custody-receipts?addr=<your-payout-address>'

Each element has four fields: seq, type, payload and sig. Two types appear. custody.time records a tip arriving and how long the pool took to put fresh work on the wire, and is pool-wide. custody.hash records the work counted for one address over a window.

5. A verifier you can run

Roughly thirty lines, two dependencies, no NexusPool code. It reads the key from the well-known document rather than trusting a constant, which is the point: nothing here asks you to take our word for a value.

import { sha256 } from '@noble/hashes/sha2.js'
import { schnorr } from '@noble/curves/secp256k1.js'

const doc = await (await fetch('https://nexuspool.io/.well-known/nexuspool-custody.json')).json()
const key = doc.authorities.find(a => a.chain === 'btc').pubkey
const enc = new TextEncoder()
const hex = h => Uint8Array.from(h.match(/../g).map(x => parseInt(x, 16)))

function msg32(type, payload, seq) {
  const d = enc.encode(doc.domain), t = enc.encode(type), p = enc.encode(payload)
  const m = new Uint8Array(d.length + 8 + 1 + t.length + 2 + p.length)
  let o = 0
  m.set(d, o); o += d.length
  let s = BigInt(seq)
  for (let i = 0; i < 8; i++) { m[o++] = Number(s & 0xffn); s >>= 8n }
  m[o++] = t.length; m.set(t, o); o += t.length
  m[o++] = p.length & 0xff; m[o++] = (p.length >> 8) & 0xff
  m.set(p, o)
  return sha256(m)
}

const { receipts } = await (await fetch('https://nexuspool.io/api/custody-receipts?addr=sample')).json()
for (const r of receipts) {
  const ok = schnorr.verify(hex(r.sig), msg32(r.type, r.payload, r.seq), hex(key))
  if (!ok) throw new Error('receipt ' + r.seq + ' does not verify')
}
console.log(receipts.length + ' receipts verified')

If a receipt fails, change one character of its payload and run it again. A verifier that still says valid is not checking what it thinks it is checking.

Where this fits

The browser version of this lives on Glass Ledger, which does the same arithmetic in the page and shows the preimage byte by byte. The rest of the machinery is on Technology.

Audit the receipts: verify NexusPool with your own code · NexusPool