Proof format
The public inputs, the Merkle sum tree, the circuit's checks, the growth bound, the limits and the proving toolchain.
A Solva proof commits to four public inputs, and the circuit and the registry each check it. Use this reference when you verify a proof yourself, build a compatible prover or read the circuit.
The claim is reserve coverage. Total reserves R are at least total liabilities L, the sum of the balances an institution owes its customers.
Public inputs
The circuit in circuits/solvency/src/main.nr declares four public inputs, in this order:
| Index | Name | Meaning |
|---|---|---|
| 0 | R | Total reserves, integer minor units. |
| 1 | root_h | Root hash of the Merkle sum tree over the liabilities. |
| 2 | L | Total liabilities, integer minor units. |
| 3 | R_prev | Reserve total of the previous proof, for the growth bound. |
Each input is a BN254 field element written as 32 bytes, big endian. The four are concatenated into a 128-byte blob, which is the public_inputs file bb writes. The order is load bearing: the contract rebuilds the same blob from its PubInputs struct before it verifies, and a different order fails verification.
The contract stores R, L and the prover's prev_reserves as u128. When it builds the blob, each u128 fills the low 16 bytes of its 32-byte word and the high 16 bytes are zero. root_h is copied as its 32 bytes.
The circuit's signature:
fn main(
R: pub Field,
root_h: pub Field,
L: pub Field,
R_prev: pub Field,
leaf_ids: [Field; N],
leaf_balances: [Field; N],
merkle_paths: [[Field; DEPTH]; N],
reserves: [Field; M],
)The private witness is each customer's id hash (leaf_ids), each customer's balance (leaf_balances) and each reserve figure (reserves). merkle_paths is part of the declared interface but the circuit does not use it: for a complete tree it recomputes the root from all the leaves, which covers every path. The prover fills it with zeros.
The Merkle sum tree
The liabilities are committed in a Merkle sum tree. Every node carries a hash and a sum. A leaf is one customer. The root's sum is L and the root's hash is root_h.
Hashing is Poseidon2 over BN254 with four inputs, Poseidon2::hash(inputs, 4) in Noir. The parameters are a width of 4 (rate 3, capacity 1), 8 full rounds, 56 partial rounds and an x^5 S-box. The prover, the circuit and the contract's native host function all use this instance.
pub fn hash4(inputs: [Field; 4]) -> Field {
Poseidon2::hash(inputs, 4)
}A leaf commits to the customer's id hash and balance:
leaf.hash = hash4([id_hash, balance, 0, 0])
leaf.sum = balanceA parent binds both child hashes and both child sums:
parent.hash = hash4([left.hash, left.sum, right.hash, right.sum])
parent.sum = left.sum + right.sumBinding the sums into the hash stops a subtree from reporting a smaller total than its leaves hold. Change any balance and the root hash changes. Lower any sum and the parent hash no longer matches.
The id_hash is a Poseidon2 commitment to the customer's own reference, computed before the cycle and stored with the customer. The tree is complete: it always has N = 8 leaves and depth 3. The prover pads a shorter list with leaves of id 0 and balance 0, which add nothing to the sum.
What the circuit checks
The circuit asserts five things. Each failure message below is the exact string in main.nr.
- Coverage.
R >= L, compared as unsigned 64-bit integers. Message:insolvent: reserves below liabilities. - The commitment. The root rebuilt from
leaf_idsandleaf_balanceshas hashroot_hand sumL. Messages:root hash mismatch,root sum does not equal L. - The liability sum. The leaf balances add up to
L. Message:leaf balance sum does not equal L. - The reserve sum. The reserve figures add up to
R. Message:reserve sum does not equal R. - The growth bound.
10 * R <= 11 * R_prev. Message:fraud bound: reserves grew too fast.
R, L, R_prev, every leaf balance and every reserve figure is range checked to fit in a u64 before it is used. Without the range check, a field element that wraps around could make a sum look smaller than it is. Message: range check failed: value exceeds u64.
The growth bound
The fifth check caps how fast reserves can rise between two proofs: R may be at most 10% above R_prev, computed in integers as 10 * R <= 11 * R_prev. The code calls this the fraud bound, because it stops a sudden jump in reserves from being proven in one cycle. There is no lower bound. Reserves can fall by any amount, as long as they still cover the liabilities.
R_prev is an input the prover chooses, so the bound means something only if R_prev is tied to history. The contract does this in publish_proof:
- Later proofs. The declared
prev_reservesmust equalRof the latest proof stored in the registry. - The genesis proof. A registry with no proof yet requires
prev_reservesto equalreserves_total. The first proof declares zero growth, so there is no arbitrary baseline to inflate against next time.
Either mismatch fails with contract error 6, PrevReservesMismatch.
The orchestrator follows the same rule. It reads R_prev from the last proof in its own database. When there is none, it sets R_prev = R. If the orchestrator's database and the registry disagree, for example after a database reset against a registry that already holds proofs, the publish fails with error 6.
Limits
| Limit | Value | Enforced in |
|---|---|---|
| Customer balances per proof | 1 to 8 | N in main.nr; checked by the prover before proving |
| Reserve figures per proof | 1 to 4 | M in main.nr; checked by the prover |
| Tree depth | 3 | DEPTH in main.nr |
| Size of any balance or total | fits in u64 | Circuit range checks; the prover parses balances as u64 |
| Units | integer minor units, decimal strings on the wire | Every tier; never floating point |
These are fixed at compile time. Larger proofs Coming soon need a new circuit build, a new verifying key and new registries.
For a token issued as a Soroban contract, the SEP-41 token interface has no total-supply call. Liabilities from token events Coming soon rebuild the amount owed to holders of such a token from its mint, burn and clawback events.
The proving toolchain
| Component | Version or setting |
|---|---|
Noir nargo | 1.0.0-beta.9 |
Barretenberg bb | 0.87.0 |
| Proving system | UltraHonk over BN254, --scheme ultra_honk |
| Transcript hash | keccak, --oracle_hash keccak |
| On-chain verifier | crates/ultrahonk-soroban-verifier, vendored from yugocabrio/rs-soroban-ultrahonk at 661db07200f890b1bd9a7349ed787c70a706dd12 |
The on-chain verifier implements the keccak transcript only, so every proof and verifying key must use --oracle_hash keccak. The flag sets the Fiat-Shamir transcript hash. It does not change the in-circuit hashing, which stays Poseidon2. A different Noir or Barretenberg version produces a different verifying key and a different proof encoding.
The prover runs these two commands in circuits/solvency for each cycle, after it writes Prover.toml:
nargo execute
bb prove --scheme ultra_honk --oracle_hash keccak \
--bytecode_path target/solva_solvency.json \
--witness_path target/solva_solvency.gz \
--output_path target --output_format bytes_and_fieldsTo produce the verifying key, and to check a proof locally:
bb write_vk --scheme ultra_honk --oracle_hash keccak \
--bytecode_path target/solva_solvency.json \
--output_path target --output_format bytes_and_fields
bb verify --scheme ultra_honk --oracle_hash keccak \
--proof_path target/proof \
--public_inputs_path target/public_inputs \
--vk_path target/vk| Artifact | Size | Role |
|---|---|---|
vk | 1,760 bytes | Verifying key, stored in the registry by __constructor. |
proof | 14,592 bytes (456 field elements) | The proof passed to publish_proof. The contract rejects any other length. |
public_inputs | 128 bytes | The four public inputs. |
The committed verifying key the registries are deployed with is contracts/proof-registry/src/testdata/solvency_vk.bin. The sample witness circuits/solvency/Prover.toml.example has eight customers owed 10 to 80 (L = 360), reserves [200, 200, 0, 0] (R = 400) and R_prev = 400. Its root is 0x0e36888d7cade7e79309cd7e58109611104c225f2fcd5a158c662debb173572f, the value just parity-check asserts in all three layers.
Next: Passkey wallet.