THE HIVEThe desktop is open. Mine a private token in your browser
propolis

Docs / Protocol / Minting

Minting

A Propolis token is a Privacy Pass token whose price is proof of work. The issuer signs it blind, so it never sees the token it signed.

Tokens

Propolis tokens are Privacy Pass tokens, publicly verifiable type 0x0002 from RFC 9578: blind RSA with a 2048-bit key, RSASSA-PSS with SHA-384. The issuer name is propolis, the origin info is ["propolis"] and the redemption context is empty, so the challenge is the same for everyone and nothing about a holder can be carried in it. A token is 354 bytes. Its 32-byte nonce is chosen by the holder when the request is made, and becomes the nullifier when the token is spent.

The key id is the SHA-256 hash of the issuer public key in SPKI form. It is what the ledger pins.

The seed

Each token request has its own seed, computed from the key id and the blinded message:

text
seed = SHA-256( utf8("propolis/mint/v1") || keyId (32 bytes) || blindedMsg (256 bytes) )

Work is done against that seed, so shares mined for one request are worthless for any other. Nobody can take your work and use it for their own token.

Shares and work

A share is an 8-byte nonce, two big-endian 32-bit words hi and lo, such that SHA-256(seed || hi || lo) starts with at least bits zero bits. The message is 40 bytes, one SHA-256 block, which is what makes the GPU kernel fast.

  • All shares of one request use the same difficulty bits, declared once, between 27 and 40. Each share is worth 2^bits expected hashes.
  • A request is accepted when distinct shares × 2^bits ≥ 2^35, about 34 billion hashes. That is roughly 40 seconds on an Apple M-series GPU and a few minutes on integrated graphics.
  • At most 256 shares per token and 8 tokens per mint call.

One declared difficulty per request keeps the credit honest: if each share could claim its own difficulty, a miner could count lucky shares above their target and get more than one token per unit of work. The issuer checks each share with a single hash.

The mint call

json
POST /api/v1/mint
{
  "keyId": "4b2b1ee3…dc47",
  "requests": [
    {
      "tokenRequest": "<base64url TokenRequest>",
      "bits": 30,
      "nonces": ["00a1b2c3d4e5f607", "…"]
    }
  ]
}

200 { "keyId": "…", "responses": ["<base64url TokenResponse>"], "minted": 1 }
StatusErrorMeaning
400bad_requestThe body or a token request does not parse
409wrong_keyThe request was made for another key; the current key id is returned
422insufficient_workThe distinct shares do not reach 2^35 expected hashes
429slow_downToo many calls from one place in a short time

Why replay gains nothing

Signing the same blinded message twice gives the same signature, so the same token. Sending a mint request again yields a token you already have, whose nullifier works once. The issuer therefore keeps no state per request: nothing to store, nothing to leak.