The receipt format

A receipt is only evidence if somebody other than us can check it. This is how one is hashed, so that software we did not write can take an evidence bundle and decide for itself whether the chain holds.

The rule

A receipt’s hash is SHA-256 over the RFC 8785 canonical JSON of exactly these fields, in this order (though order does not matter, because canonicalisation sorts keys):

{
  "actor":       string,       // who caused this entry
  "env":         "live" | "simulated",
  "event_type":  string,       // held, released, investigated, settled, …
  "org_id":      uuid,         // the organisation the receipt belongs to
  "payload":     object,       // the entry's own content
  "payment_id":  uuid,
  "prev_hash":   string | null,// the receipt before it in this chain
  "seq":         integer       // 1-based, per payment
}

Objects serialise with sorted keys, arrays in order, strings as JSON. Non-integer numbers are refused, not rounded. Amounts travel as fixed-point strings ("12800.00", never 12800.0), so canonicalisation is exact by construction rather than by convention. A hash that depends on how a float printed is not a hash anybody can reproduce.

Checking a chain

Two things, and both are needed:

  1. Each receipt’s stored hash equals the rule above applied to its own fields. This catches a receipt whose contents changed after it was written.
  2. Each receipt’s prev_hash is the previous receipt’s hash, and the first is null. A set of individually valid receipts in the wrong order is not a chain.

An evidence bundle carries every field each hash binds, so it can be checked offline with no access to us, no database and no network.

Why org_id is in the hash

Recurro is multi-tenant, so a receipt commits to the organisation it belongs to. Without it, a valid receipt could be presented as another organisation’s. Every field would check out, and the claim would still be false.

Signing

A hash proves a receipt has not changed since it was written. It cannot prove who wrote it: anyone who can write the row can compute its hash. The signature is the other half.

  1. The key is Ed25519, derived in the payer’s browser from their passphrase and the organisation’s id. The private half is never transmitted and never stored. We could not produce one of these signatures if we wanted to. That is the only reason it is worth more than our word.
  2. The signed message is the ASCII string recurro-receipt-v1: followed by the receipt’s lower-case hex hash. The prefix is domain separation: without it, a signature over any other 32 bytes could be presented as a signature over a receipt.
  3. A bundle carries signature, signed_by and the rawsigning_public_key for each receipt. Publishing the public key is safe: it verifies and cannot sign.

A receipt with no signature is a real state, not an error. A payment never waits for someone to be ready to type a passphrase. Bundles report receipts_signed separately fromchain_intact, because “nothing changed” and “here is who wrote it” are different facts and one does not imply the other.

Revoking a key stops it signing anything further. Signatures it already made stay valid: a receipt signed in March is evidence about March, and withdrawing that would destroy the record rather than protect it.

What the amount was calculated from

A receipt’s payload carries the rate, the quantity, the unit and the currency the contract is written in. When that differs from what was settled, it carries the conversion used and where it came from. It also names the invoice or contract the payment answers, with a SHA-256 of that file. All of it is inside the payload, so the hash and the signature cover it.

Rates multiply as scaled integers and round half-up, the way an invoice does. A line item that does not reconcile to the amount is refused rather than recorded: one of the three numbers is wrong, and evidence that contradicts itself under examination is worse than no evidence.

The document itself is never sent to us. To check one, run shasum -a 256 over the file and compare it with document_hash in the bundle.

Disclosure keys, and what comes next

Scoped disclosure keys, which open one date range and one set of counterparties and nothing else, are separate from the signing keys above: an auditor holding one sees exactly that scope, and every use is logged for the organisation that issued it. Next is a published transparency log of enrolled keys, so a key appearing in a bundle can be checked against a record we cannot silently change.