posl docs
poslProtocol reference

Circuits

Five Groth16 circuits on BN254, all for trees of 26 levels. The public inputs below are read from each circuit's main component, in the order the program passes them.

transaction2 (spend, upstream)#

Privacy Cash's circuit, unchanged: two inputs, two outputs, amounts range-checked on 248 bits, sum(inputs) + publicAmount = sum(outputs). Public inputs: root, publicAmount, extDataHash, two input nullifiers, two output commitments.

lock#

Spends two main-tree notes and creates one change note (main) and one locked note (stake). Proves: both inputs are in the tree and owned, their nullifiers are correct and different, sum(inputs) + publicAmount = change + denom with publicAmount = -fee, change on 248 bits, denom on 64 bits, lockEpoch on 32 bits.

Template Lock(26). Public inputs, in order:

  1. root
  2. publicAmount
  3. extDataHash
  4. inputNullifier[2]
  5. changeCommitment
  6. lockedCommitment
  7. denom
  8. lockEpoch

unlock#

Spends one locked note and creates one main note. Proves ownership and membership in the stake tree, the stake nullifier, lockEpoch < currentEpoch, and denom + publicAmount = outAmount. The size and the end epoch stay private.

Template Unlock(26). Public inputs, in order:

  1. stakeRoot
  2. currentEpoch
  3. publicAmount
  4. extDataHash
  5. nullifier
  6. outputCommitment

claimStake#

Proves: I own a locked note that was already in the stake tree when epoch E opened (index < epochStartIndex) and whose lock covers E (E <= lockEpoch); here is rewardNullifier = Poseidon(signature, E); and the reward note holds floor(denom * rate / 2^32). No "not spent" proof is needed: a note locked before E with lockEpoch >= E could not be unlocked during E.

Template ClaimStake(26). Public inputs, in order:

  1. stakeRoot
  2. epoch
  3. epochStartIndex
  4. rate
  5. rewardNullifier
  6. rewardCommitment
  7. extDataHash

claimVolume#

Spends up to four credits of epoch E (all different, each on 64 bits) and creates a reward note of floor(sum * rate / 2^32), which must be above zero (IsZero(reward) === 0).

Template ClaimVolume(26, 4). Public inputs, in order:

  1. creditRoot
  2. epoch
  3. rate
  4. boostQ32
  5. boostRoot
  6. creditNullifier[N]
  7. rewardCommitment
  8. extDataHash

Shared building blocks#

  • Reward arithmetic: product = reward * 2^32 + rem with rem on 32 bits and reward on 96 bits. Amount and rate are both below 2^64, so the product fits in the field.
  • Dummy inputs: an input of amount 0 has its Merkle check disabled (enabled = 0), as in the core circuit; a non-zero amount outside the tree is refused (tested).
  • Path indices go through Num2Bits(26) in the Merkle proof, which bounds them for the whole circuit.

Tested cheating inputs#

Each rule has a test with a cheating witness that must fail on the targeted constraint, among them: a negative change that wraps around the field to lock more than one has, a dummy input with a non-zero amount outside the tree, four empty credits, a reward that rounds to zero, a changed public input on a real proof.