DEV Community

Hiroshi Ichiyanagi
Hiroshi Ichiyanagi

Posted on

openreserve: offline-verifiable proof-of-reserves for the off-chain world

"Are the reserves actually there?" has become a recurring, expensive question. A string of high-profile failures — where customer funds turned out not to fully back the liabilities — has made verifiable reserves a live concern rather than a back-office footnote. In parallel, there's a broader drift toward periodic, independently-checkable reserve attestation for some classes of issuers and custodians.

Most of the tooling people reach for here is on-chain: oracle-style proof-of-reserve systems (Chainlink's Proof of Reserve is the well-known example) designed for on-chain assets and smart-contract consumers. That's a real and useful domain — but it's not the one a payment processor, e-money issuer, wallet, or custodian lives in. Their ledger is in a normal database; their auditors and regulators are off-chain; an on-chain oracle is the wrong shape. openreserve is a small attempt at the off-chain gap.

What it is

openreserve is a dependency-free Python library (standard library only); install it with pip install openreserve. Given a ledger state — an append-only, double-entry ledger — and an explicit point in time, it produces and verifies:

  • Proof of Reserves — a Merkle tree over user-account balances plus reserve-account totals, with a solvency check (reserves >= liabilities). A user can verify their own balance is included via a Merkle proof, without the operator revealing other users' balances.
  • Proof of Solvency — per-currency proof-of-reserves aggregated with the audit chain's commitment hash.
  • Audit chain — a hash-linked event log (prev_hash → event_hash) with a verify_chain() that detects any tampering with past entries.
  • Reserve / deposit calculation — point-in-time required-reserve computation (settled balances plus in-flight obligations; negative balances excluded).

The artifacts are plain data. A proof's public summary is a JSON-serializable dict, so you can publish it at a static URL and a third party can fetch it and re-verify it offline using the same library primitives. openreserve does not ship an HTTP serving layer — exposing an endpoint is the integrator's job.

Proof of Reserves vs Proof of Solvency

These two are often conflated, so to be precise:

  • Proof of Reserves — evidence that the assets (reserves) actually exist / are held.
  • Proof of Solvency — evidence that assets ≥ liabilities: the reserves actually cover what is owed.

openreserve does both: a Merkle-committed reserve proof, plus a per-currency solvency check (reserves >= liabilities). The solvency claim is only as complete as the liabilities you include — see Limitations.

Who it's for

The common shape is an operator whose balances are an off-chain liability in a normal database, and whose auditors, counterparties, or regulators are off-chain:

  • E-money / prepaid-balance issuers — customer balances are liabilities that must be backed.
  • Payment processors / PSPs holding client or settlement funds.
  • Custodians, including the off-chain customer-balance ledger of a crypto exchange (the familiar "CEX proof-of-reserves" case).
  • Stablecoin issuers attesting the off-chain reserve backing (the reserve side, held in banks/custody — not the on-chain token).
  • Loyalty-point / closed-loop wallet operators — outstanding points are an off-chain liability.

In every case the question is the same: can a third party verify the reserves cover the off-chain liabilities, without trusting a dashboard?

The core property: determinism

The reason any of this is worth doing is that the proofs are deterministic. Proof generation takes the as-of / event time as an explicit, required input and never reads the wall clock. The same ledger state and the same timestamp produce a byte-identical Merkle root and audit hash.

This is the difference between "trust our dashboard" and "verify our proof." If the output depended on datetime.now(), no outsider could reproduce it. Because it doesn't, a regulator, auditor, or counterparty can take a published proof and check it themselves.

Don't trust me, verify me

The verification loop is one-directional — the operator publishes, anyone re-checks offline:

OPERATOR SIDE
  ledger state + explicit as-of time
        |
        v
  generate proof            (deterministic: same state + time -> same hashes)
        |
        v
  publish proof as JSON     (static URL or file)
        |
========|=====================================================
        v
VERIFIER SIDE  (auditor / user / regulator -- offline, no trust in operator)
  fetch proof JSON
        |
        +--> recompute Merkle root
        +--> verify audit chain   (prev_hash -> event_hash)
        +--> check solvency       (reserves >= liabilities)
        +--> (a user) verify own balance is included via Merkle proof
Enter fullscreen mode Exit fullscreen mode

The repo ships a determinism guard you can run yourself (clone the repo and run the test suite):

python -m pytest tests/test_determinism_guard.py
Enter fullscreen mode Exit fullscreen mode

It asserts (a) the same ledger state + same snapshot_at reproduces the same Merkle root and audit hash, and (b) with datetime.now patched to raise, generation with an explicit time still succeeds — i.e. the wall clock is genuinely never touched.

A minimal end-to-end example (after pip install openreserve):

from datetime import datetime, timezone
from openreserve import (
    Currency, Ledger, Money, OwnerType,
    ProofOfReservesGenerator, SQLiteLedgerStorage, TransactionBuilder,
)

T = datetime(2026, 1, 1, tzinfo=timezone.utc)  # explicit time — no wall clock
ledger = Ledger(SQLiteLedgerStorage(":memory:"))
platform = ledger.open_account(OwnerType.PLATFORM, Currency.JPY, "platform")
reserve = ledger.open_account(OwnerType.RESERVE, Currency.JPY, "reserve")
alice = ledger.open_account(OwnerType.USER, Currency.JPY, "alice")

def fund(src, dst, units):
    b = TransactionBuilder("FUND", "ops", initiated_at=T)
    b.transfer(src.account_id, dst.account_id, Money.from_units(units, Currency.JPY))
    ledger.post(b.build()); ledger.settle(b.transaction_id, settled_at=T)

fund(platform, reserve, 1_000_000)
fund(platform, alice, 300_000)

proof, _ = ProofOfReservesGenerator(ledger).generate(currency=Currency.JPY, snapshot_at=T)
print(proof.is_solvent, proof.user_liabilities_total_cents, proof.reserve_assets_total_cents)
# True 300000 1000000
Enter fullscreen mode Exit fullscreen mode

Limitations

Being precise about what is and isn't guaranteed:

  • Determinism is on the time axis. Account and transaction identifiers are UUID-based, so two ledgers rebuilt independently won't share identifiers or hashes; identifier stability is not claimed. What is claimed: a given ledger state, at a given time, is reproducibly provable, and any published proof is independently verifiable. (Content-addressed, rebuild-stable identifiers are on the roadmap.)
  • Liabilities completeness. Like all proof-of-reserves, it proves assets cover the liabilities you include; it can't force you to include every liability. Per-user Merkle inclusion raises the cost of silently omitting someone, but it is not a substitute for an attestation of completeness.
  • No HTTP / serving layer. Publishing a proof at an endpoint is the integrator's job; the library only produces and verifies the artifacts.
  • No on-chain anchoring. A proof isn't committed to a blockchain today (it could be layered on top); the core stays off-chain.
  • Early and unproven. v0.1.1, no production adoption, APIs may change.

What it is not

  • Not on-chain, and not a replacement for or competitor to oracle-style PoR. Different domain, different consumers.
  • Not a compliance product. It makes no claim to satisfy any specific regulation, and nothing here is legal or financial advice. The regulatory climate is context, not an endorsement.
  • Not novel cryptography. It's a clean integration of standard primitives (Merkle trees, hash chains) with a determinism discipline — the value is in the assembly and the verifiability, not in inventing new math.

Status & roadmap

The determinism guarantee is on the time axis; UUID identifiers are the obvious next thing to make deterministic if cross-rebuild reproducibility is needed. There's no HTTP/serving layer and no on-chain anchoring (both could be layered on top). CI runs the suite on Python 3.11–3.13 plus a packaging check.

Apache-2.0. On PyPI (pip install openreserve); repo and issues: https://github.com/Hiroshi-Ichiyanagi/openreserve

Top comments (0)