Use
examples/testnet/income. It reveals which
outputs were counted and each note’s nullifier, and nothing else.
Requiring separate payments and separate payers
--min-payers needs payer tags from the payers. That makes the
total one that came from several wallets.
How it works
For each counted output the verifier reads the note commitment from the chain and builds a one-leaf Merkle tree from it. The recipient makes an Action proof against that tree, with the value commitment fixed by the verifier toValueCommit_0(floor). The circuit’s 64-bit range checks on the note’s
value and on the hidden output’s v − floor give v ≥ floor, and floor ≥ 1 forces the anchor
check. One SpendAuthSig by rk covers every item.
The proof shows the sum of the floors, not the amounts.
Limits
- It proves at least the sum of the floors the recipient chose.
- Payments appear as outputs with nullifiers, so the verifier knows how many were counted.
- Whoever gives the lender the proof chose the payments; a lender who needs a full history asks for a viewing key, which is the thing receipts avoid.
crates/zceipt-core/src/income.rs. Test:
roundtrip.rs::proof_of_income_shows_a_total_not_the_amounts.