Skip to main content

Proof of Solvency methodology v1

Every day Rocky publishes a cryptographic commitment proving that custodied platform assets ≥ total user liabilities, and lets every user verify, without trusting the platform, that their own balance is fully included. This page is the complete specification of that mechanism. Once published, the methodology is versioned: any change increments the version and keeps the history.

Status (2026-09-08): live in production. solvency-prover runs daily at 00:30 UTC and the first report (liabilities_only) was published on 2026-09-08; GET /v1/solvency/latest returns it without login. The custody (asset) side is not yet included, see §4.

  • Public report page: /solvency on the exchange (the Transparency entry in the top navigation is temporarily hidden; the page and route remain and can be opened directly)
  • Public API: GET /v1/solvency/latest (report, no login) and GET /v1/solvency/proof/me (your own proof, Authorization: Bearer <session_token>)
  • Reference implementation (byte-for-byte what runs in production): rocky-backend crates/solvency-merkle (tree and hashes) and services/solvency-prover (snapshot and report)

1. Snapshot​

Every day at 00:30 UTC (SNAPSHOT_UTC of solvency-prover, default 00:30) a single-transaction (REPEATABLE READ) consistent snapshot of the internal ledger is taken, together with the event-stream high-water mark (the id of the newest event in ledger.events), which pins the snapshot to a definite point in event history.

The per-contract prices used for unrealized PnL are read at the snapshot instant from the exchange index-price stream (oracle.index_price.>, published by oracle-poller) and written into the report verbatim (mark_prices), so anyone can recompute. If the price for any contract with open positions is missing or stale, the snapshot fails rather than publishing with an old price.

2. Liabilities​

Liabilities = Σ net equity per user per asset, made up as follows:

ItemTreatment
Perp margin balances (ledger.accounts)available + locked, counted in full
Spot balances (ledger.spot_balances)free + locked, counted in full
Unrealized PnL (ledger.positions)folded into the margin asset at the snapshot price. Omitting it would systematically understate liabilities
Realized PnL, fundingalready reflected in balances; not double-counted
In-flight withdrawals (pending / submitted)counted as liabilities (assets have not left custody, the user has not received them)
In-flight depositsexcluded on both sides to keep the inequality symmetric
Insurance fund (ledger.insurance_fund)not a user liability; disclosed separately (insurance_fund), including any shortfall when negative
House accounts (Rocky's own market-making and taker fleet, i.e. API keys on the house rate tier)excluded from liabilities (platform equity); the number excluded and their totals are disclosed (house_excluded, house_totals). External market makers are ordinary users and stay in the liability set.
Negative-equity accountsclamped to 0; the negative total is disclosed separately as bad debt (bad_debt)
Zero balancesomitted from the leaf; shortfalls appear only through bad debt

3. Assets​

Assets = the actual on-chain holdings of the exchange custody party on the Canton ledger, per asset, read at the same instant as liabilities. On-chain contracts belonging to in-flight deposits are excluded as well.

The custody verification module has not shipped yet: today the report's assets field is null and status is liabilities_only. Once it ships, status becomes complete, or degraded if the asset side could not be read.

4. Commitment structure: Merkle Sum Tree​

  • Leaf: SHA-256("rocky-solvency-leaf-v1" ‖ salt ‖ SHA-256(user_id) ‖ canonical(balances))
    • salt is a per-user 32-byte salt: HMAC-SHA256(master_salt, user_id). The master salt is the SOLVENCY_MASTER_SALT setting of solvency-prover (hex-encoded, at least 16 bytes, rotated by operations, not derived per day) and is revealed only to you, inside your own proof;
    • canonical(balances) is the list of ASSET:amount pairs sorted bytewise by asset name, amounts fixed at 18 decimals, joined by |, e.g. CC:12.000000000000000000|CUSD:3.500000000000000000.
  • Internal node: SHA-256("rocky-solvency-node-v1" ‖ left_hash ‖ right_hash ‖ canonical(sums)), where sums is the per-asset sum of the two subtrees. The root's sums are the published liability totals, so a verifier can confirm the published totals really aggregate every leaf; the platform cannot leave a user out.
  • An odd node is promoted to the next level unchanged (never duplicated, which rules out double counting); any sum overflow is an error; non-negative leaf amounts are the structural safety precondition (see the clamp in §2).

5. API​

5.1 GET /v1/solvency/latest​

Returns the report for the most recent snapshot:

FieldDescription
report_id, snapshot_atReport id and snapshot time (UTC)
event_high_waterEvent-stream high-water mark at the snapshot
root_hashMerkle Sum Tree root hash (hex)
liabilitiesThe root's sums: total liabilities per asset
assetsCustody party on-chain holdings; null until custody verification ships
mark_pricesPer-contract prices used by the snapshot
insurance_fund, bad_debt, house_excluded, house_totalsDisclosures from §2
user_countNumber of leaves
statusliabilities_only / complete / degraded

Returns 404 while no report exists.

5.2 GET /v1/solvency/proof/me​

Returns the inclusion proof of the signed-in user in the latest report:

FieldDescription
report_id, snapshot_at, root_hashThe report the proof belongs to
leaf.user_id, leaf.leaf_indexYour leaf position
leaf.balancesYour net equity per asset at the snapshot
leaf.saltYour derived salt (hex)
leaf.leaf_hashThe leaf hash computed by the server (hex)
path[]Sibling nodes from leaf to root: sibling_hash, sibling_sums, sibling_on_left

6. Verify it yourself​

Sign in, open /solvency and click Verify my balance. The browser performs three steps locally (see src/shared/lib/solvency/verify.ts in the frontend; nothing computed is sent to the server):

  1. Recompute your leaf hash from the disclosed salt and balances and compare it with the leaf hash the server returned;
  2. Combine along the sibling path level by level and compare with the published Merkle root;
  3. If the public report and the proof belong to the same snapshot, compare the liability totals derived from the path with the published totals.

All three passing means your balance is included faithfully and counted in the published totals. Any tampering (altering your balance, omitting you, overstating the totals) makes at least one step fail.

7. Known limits​

  1. The asset side is the custody party's on-chain holdings and excludes off-chain assets (Rocky is fully on-chain today, so this does not apply); until the custody verification module ships the report covers the liability side only.
  2. Unrealized PnL depends on the index prices the platform publishes; oracle-poller supports Chainlink Data Streams as a price source (PRICE_SOURCE=chainlink), and once enabled the prices themselves become third-party verifiable.
  3. Snapshots are daily; nothing is committed in between, so intraday coverage swings in extreme markets are not captured by that day's report.
  4. "No user was omitted" relies on users spot-checking their own proofs (the classic PoR limitation); the verification entry stays on the page to raise the spot-check rate.

8. Versions​

VersionDateChange
v12026-08Initial release