GpuFiles
Many lines, one root

Blind notes

A note is a bearer claim on 0.004 USDG, signed by the pool without the pool ever seeing it. It is how a miner gets paid without a wallet, an account, or a link between the mining session and the payout.

BLINDED MESSAGEBLIND SIGNATUREFINALIZENOTE + ADDRESSNO LINK BETWEENTHE SESSION ANDTHE WALLETMINER TABVERIFIED WORK100 FRAGMENTSISSUERSIGNS BLINDSEES NO NOTENOTEUNBLINDEDIN THE TABANY WALLETREDEEMSLATER, ELSEWHERENULLIFIERSSPENT ONCEDOUBLE SPEND REFUSEDPAYOUT CONTRACTHOURLY BATCHUSDG + MERKLE ROOT

Issuance

The suite is RSABSSA-SHA384-PSS-Randomized from RFC 9474 (RSA blind signatures), with a 2048-bit issuer key published at/api/v1/notes/key. For each note the miner's tab:

  • draws 32 random bytes and prepares them (the randomized variant adds a 32-byte prefix),
  • blinds the prepared message with the issuer's public key and a fresh blinding factor,
  • sends the blinded bytes with its session id to /api/v1/notes/mint.

The pool checks the session has 100 unspent verified shares per note, subtracts them in the same statement, and signs the blinded bytes. The tab unblinds the result into a standard RSA-PSS signature over the prepared message. The pool has seen only the blinded bytes, which are uniformly random to it.

Redemption

Anyone holding notes posts them with a payout address to /api/v1/notes/redeem, from any browser or script, at any time. The pool verifies each signature, then inserts each note's nullifier, the SHA-256 of its message, into a table with a unique key. A note whose nullifier is already there is refused: a note pays once. Up to 500notes per request.

Unlinkability, in practice

The signature hides the session. Timing and amounts can still correlate, so: redeem to an address that has never been used with this browser, wait some hours after a session, and redeem round numbers of notes. Notes can be exported as text and redeemed from another machine on /notes.

Verify a note yourself

const key = await fetch("/api/v1/notes/key").then(r => r.json());
const pk  = await crypto.subtle.importKey("spki", b64(key.spki),
              { name: "RSA-PSS", hash: "SHA-384" }, false, ["verify"]);
const ok  = await crypto.subtle.verify({ name: "RSA-PSS", saltLength: 48 },
              pk, b64(note.s), b64(note.m));