SolvaDocs

Check a balance was counted

How a holder confirms on Stellar that their own balance was in the list a coverage record covered, with the Stellar CLI or the SDK.

A record says that reserves covered everything an institution owed its customers. The inclusion check lets one holder confirm that their own balance was part of that total, on Stellar, without trusting the institution's word.

Holders who do not write code can run the check in their institution's app, where the app offers it, or on its public record page Coming soon. To watch one run end to end, open the demo at joinsolva.xyz/demo, where an example holder is checked on the contract with verify_inclusion.

What the check proves

Before proving, Solva's prover turns the institution's list of customer balances into a tree of hashes. Each customer is a leaf, a hash of the customer's id hash and balance. Each step up the tree combines two branches and their subtotals. The top of the tree is the commitment root, which the registry stores in the record with the total owed.

Your inclusion path holds the branch beside yours at each level, with its subtotal. With your leaf and your path, anyone can rebuild the root.

The registry's verify_inclusion function does that rebuild on Stellar. It returns true only when both of these hold:

  • the rebuilt root equals the commitment root stored in the record,
  • the running total at the top equals the record's total owed.

A true result means your balance, exactly as given, was in the list that the proof covered with reserves. The institution cannot have counted you at a lower balance, and it cannot have left you out, because a changed or missing leaf rebuilds a different root.

A false result means the details do not match that record. Check that you used the right record and copied every value exactly. If it still fails, raise it with the institution.

What the check reveals

The check never reveals another customer's name, id or account. The path holds only hashes and subtotals of neighbouring branches, and the check is a read that writes nothing to the ledger.

At the current tree size of 8 customers, the first step of a path is a single neighbouring leaf, so its subtotal is one other customer's balance, without saying whose. Higher steps are sums of several balances. See Privacy.

What you need

ItemWhat it isWhere it comes from
Registry contractThe institution's registry contract idFind a record
Proof idThe number of the record you are checking against, in the registryThe institution
Id hashThe Poseidon2 hash of your customer reference, your leaf's label in the tree, 32 bytes in hexThe institution
BalanceYour balance as committed, in the asset's smallest unitThe institution
Inclusion pathOne step per level of the tree, with the sibling's hash, its subtotal and which side it sits onThe institution

The institution serves these from Solva's orchestrator at GET /v1/proofs/inclusion/{proof}:{id-hash}. {proof} is the orchestrator's id for the proof, and {id-hash} is your id hash. The response is JSON:

FieldMeaning
proof_idThe proof id in the registry, the number verify_inclusion takes
customer_id_hashYour id hash, as 0x followed by 64 hex characters
balanceYour committed balance, as a decimal string
root_hashThe commitment root the path leads to, as 0x followed by 64 hex characters
pathThe steps, each with hash, sum and sibling_is_left, in the same shape the contract takes

The steps below assume you saved that response as inclusion.json. They use the Solstice Exchange example registry.

Run the check with the Stellar CLI

You need the Stellar CLI, a funded Testnet key named reader (see Check a record on Stellar), and jq to read the JSON file.

Confirm the root matches the record

Read the record your path points at, and compare its root_h with the root_hash in your file:

stellar contract invoke \
  --id CDSQ35U2Y3VC44PJC6RRUJVCES2DLBWELDN32AIAEY4J35H4WBEKEUQW \
  --network testnet \
  --source-account reader \
  -- get_proof --id "$(jq -r .proof_id inclusion.json)"

The registry prints the root without the 0x prefix. The two values must be the same 64 characters.

Call verify_inclusion

The CLI takes hashes as bare hex, so the command strips the 0x prefix from each one:

stellar contract invoke \
  --id CDSQ35U2Y3VC44PJC6RRUJVCES2DLBWELDN32AIAEY4J35H4WBEKEUQW \
  --network testnet \
  --source-account reader \
  -- verify_inclusion \
  --id "$(jq -r .proof_id inclusion.json)" \
  --id_hash "$(jq -r '.customer_id_hash | ltrimstr("0x")' inclusion.json)" \
  --balance "$(jq -r .balance inclusion.json)" \
  --path "$(jq -c '[.path[] | .hash |= ltrimstr("0x")]' inclusion.json)"

Read the answer

The contract prints one word:

true

true means your balance was counted in that record. false means the details given do not match it.

If the proof id does not exist in the registry, the call fails with error 5, ProofNotFound, instead of returning an answer.

Run the check with the SDK

The SDK's verifyInclusion calls the same contract function. Given the full details as an object, it goes straight to the registry and makes no call to Solva's API. It takes each step's side as position, which is "left" when the sibling sits on the left. Hashes may keep their 0x prefix.

check-balance.ts
import { readFile } from "node:fs/promises";
import { Solva } from "@solva/sdk-ts";

const inclusion = JSON.parse(await readFile("inclusion.json", "utf8"));

const solva = new Solva({
  network: "testnet",
  tenant: "22222222-2222-2222-2222-222222222222",
  endpoints: { contractId: "CDSQ35U2Y3VC44PJC6RRUJVCES2DLBWELDN32AIAEY4J35H4WBEKEUQW" },
});

const result = await solva.verifyInclusion({
  proofId: inclusion.proof_id,
  customerIdHash: inclusion.customer_id_hash,
  balance: inclusion.balance,
  path: inclusion.path.map((node) => ({
    hash: node.hash,
    sum: node.sum,
    position: node.sibling_is_left ? "left" : "right",
  })),
});

console.log(result);

The result names the record it checked against:

{ "included": true, "proofId": "4" }

verifyInclusion also accepts a reference string, "<proof>:<id-hash>". The SDK then fetches the path from the orchestrator first, which needs access to the institution's orchestrator. Passing the full object keeps the check between you and Stellar.

What a holder still has to trust

The check proves your balance was in the list. It does not prove that the reserves figure is honest. That rests on the reserve source: the custodian's signature, or the Stellar ledger for reserves read on chain. See Where reserve figures come from.

Next: For exchanges and wallets.

On this page