Contract reference
Every public function, type, error and event of the proof-registry Soroban contract, with storage lifetime, fees and Testnet addresses.
contracts/proof-registry is the Soroban contract that holds an institution's records. Use this reference to read a record yourself, check a holder's inclusion or subscribe to new proofs.
The contract holds the verifying key, runs UltraHonk verification over BN254 with the native host functions, stores each verified proof, and serves the Poseidon2 inclusion check. It makes no cross-contract calls. Each institution has its own instance, its registry.
Addresses on this page are Stellar strkeys as defined in SEP-23: contracts start with C, accounts with G.
Functions
__constructor
fn __constructor(env: Env, owner: Address, vk: Bytes) -> Result<(), Error>Runs once, at deploy. It stores owner, the account or smart wallet allowed to publish, and vk, the circuit's verifying key, and sets the latest proof id to 0. The key cannot be changed afterwards. Returns error 1 if the contract already has an owner.
Build the contract and deploy a registry with the committed verifying key, from the repository root:
stellar contract build
stellar contract deploy \
--wasm target/wasm32v1-none/release/proof_registry.wasm \
--source tester --network testnet -- \
--owner tester \
--vk-file-path contracts/proof-registry/src/testdata/solvency_vk.binpublish_proof
fn publish_proof(env: Env, proof: Bytes, pub_inputs: PubInputs) -> Result<u64, Error>Verifies a proof and records it. Only the owner can call it. In order, it:
- Requires the owner's authorization with
owner.require_auth(). - Loads the verifying key. Error 2 if it is missing.
- Verifies
proofagainst the key, with the public inputs rebuilt frompub_inputsin the orderR, root_h, L, R_prev. A proof that is not exactly 14,592 bytes fails at once. Error 3 if verification fails. - Checks
reserves_total >= liabilities_total. Error 4 otherwise. - Checks the growth baseline. On the first proof,
prev_reservesmust equalreserves_total. After that, it must equalrof the latest stored proof. Error 6 otherwise. - Stores a
ProofMetaunder the next id, with the ledger timestamp, and sets it as the latest. - Extends the lifetime of the new proof entry and of the contract instance.
- Emits
proof_published_event.
It returns the new proof id. Ids start at 1 and increase by one per proof.
get_latest_proof
fn get_latest_proof(env: Env) -> Result<ProofMeta, Error>Returns the latest proof. Before the first publish, the latest id is 0 and the call returns error 5. Anyone can call it; a read needs no authorization.
get_proof
fn get_proof(env: Env, id: u64) -> Result<ProofMeta, Error>Returns the proof with that id, or error 5 if there is none. When the call is sent as a transaction, it also extends the entry's lifetime. A simulated read changes nothing.
verify_inclusion
fn verify_inclusion(
env: Env,
id: u64,
id_hash: BytesN<32>,
balance: u128,
path: Vec<PathNode>,
) -> Result<bool, Error>Checks that a holder's leaf is in the tree committed by proof id. id is the on-chain proof id, not the orchestrator's proof UUID. It returns error 5 if the proof does not exist.
The contract hashes the leaf as hash4([id_hash, balance, 0, 0]), then walks path from the leaf to the root. At each step it orders the current node and the sibling by sibling_is_left and hashes hash4([left.hash, left.sum, right.hash, right.sum]), adding the sibling's sum to the running sum. It returns true only if the final hash equals the stored root_h and the final sum equals the stored l. The sum uses saturating addition, so an oversized path returns false instead of failing.
Types
pub struct PubInputs {
pub reserves_total: u128,
pub liabilities_total: u128,
pub root_hash: BytesN<32>,
pub prev_reserves: u128,
}
pub struct ProofMeta {
pub root_h: BytesN<32>,
pub r: u128,
pub l: u128,
pub timestamp: u64,
}
pub struct PathNode {
pub hash: BytesN<32>,
pub sum: u128,
pub sibling_is_left: bool,
}| Field | Meaning |
|---|---|
ProofMeta.root_h | Root hash of the Merkle sum tree, 32 bytes. |
ProofMeta.r, ProofMeta.l | Total reserves and total liabilities, integer minor units. |
ProofMeta.timestamp | Ledger close time of the publish, Unix seconds. |
PathNode.hash, PathNode.sum | The sibling's hash and the total of the sibling's subtree. |
PathNode.sibling_is_left | true when the sibling is the left child and the current node the right. |
Errors
| Code | Name | Meaning |
|---|---|---|
| 1 | AlreadyInitialized | The constructor ran on a contract that already has an owner. |
| 2 | NotInitialized | The owner, the verifying key or the latest id is missing. |
| 3 | ProofInvalid | The proof is the wrong length or does not verify against the stored key and the given public inputs. |
| 4 | InsolventBound | reserves_total is below liabilities_total. |
| 5 | ProofNotFound | No proof with that id. Also returned by get_latest_proof before the first publish. |
| 6 | PrevReservesMismatch | prev_reserves does not match the registry's history: the latest r, or reserves_total on the first proof. |
The Stellar CLI and RPC report these as Error(Contract, #<code>).
Event
Each successful publish emits one event, defined in storage.rs:
#[contractevent]
pub struct ProofPublishedEvent {
pub reserves_total: u128,
#[topic]
pub liabilities_total: u128,
#[topic]
pub ts: u64,
}| Part | Value |
|---|---|
| Topic 0 | The symbol proof_published_event |
| Topic 1 | liabilities_total, u128 |
| Topic 2 | ts, the ledger timestamp, u64 |
| Data | A map with one key, reserves_total, u128 |
The event carries no proof id and no root. To get them, call get_latest_proof after you see the event.
Storage and lifetime
| Storage | Key | Value |
|---|---|---|
| Instance | Owner | Address |
| Instance | Vk | Bytes |
| Instance | LatestId | u64 |
| Persistent | Proof(u64) | ProofMeta, one entry per proof |
Soroban entries have a time to live (TTL). The contract counts 17,280 ledgers per day, about 5 seconds a ledger. When a proof is published, and when get_proof runs in a transaction, the contract extends the entry to 30 days (518,400 ledgers) if fewer than 7 days (120,960 ledgers) remain. A publish extends the instance the same way. An entry nobody extends is archived when its TTL runs out and must be restored before it can be read again.
Read a registry
Read the latest proof with the Stellar CLI. --send=no simulates the call and submits nothing, so it costs nothing. Any funded Testnet identity works as the source account.
stellar contract invoke \
--id CAFUK25F56VW442VAOHDKGMPYF2BZ3ERVLVAM4H6JTN2WGZPDUQLRTPN \
--source-account tester --network testnet --send=no \
-- get_latest_proofThe result looks like this. The values change after each cycle.
{
"l": "9000000",
"r": "16000000",
"root_h": "0c5c65b8b72f420da5a035470aa35ce6bce46a8309bd605cb10b9392d82c922e",
"timestamp": 1793685990
}Print the full interface of a deployed registry:
stellar contract info interface \
--id CAFUK25F56VW442VAOHDKGMPYF2BZ3ERVLVAM4H6JTN2WGZPDUQLRTPN --network testnetFrom TypeScript, use the SDK's getOnChainLatestProof and verifyInclusion.
Fees
Measured on Testnet and recorded in contracts/proof-registry/README.md:
| Transaction | Fee | How often |
|---|---|---|
publish_proof | about 240,693 stroops (0.0240693 XLM) | once per cycle |
UltraHonk verification alone (verify_proof on the standalone verifier) | 122,514 stroops (0.0122514 XLM) | per verification |
| Contract deploy and constructor | 210,822 stroops (0.0210822 XLM) | once per registry |
| Wasm upload | 16,567,513 stroops (1.6567513 XLM) | once per contract build |
The account that submits publish_proof pays its fee. In the demo that is the orchestrator's signer. Reads by simulation are free.
Deployed contracts on Testnet
Kestrel Anchor, Solstice Exchange and Harbor Wallet are fictional example institutions used by the demo. All three registries run the current contract build, with errors 1 to 6.
| Contract | Address |
|---|---|
| Kestrel Anchor registry | CAFUK25F56VW442VAOHDKGMPYF2BZ3ERVLVAM4H6JTN2WGZPDUQLRTPN |
| Solstice Exchange registry | CDSQ35U2Y3VC44PJC6RRUJVCES2DLBWELDN32AIAEY4J35H4WBEKEUQW |
| Harbor Wallet registry | CBOA5AJEMTQ5CSMHE6WFWWBIYYMNSUQGKWE6FCFB2KKNJSJPHBWSCSJK |
| Standalone verifier | CA3KQYYAGUEIURJSB5CSWVMNIY2FF2EG3YTM7SBBRSN7DQWZXCCXWXN3 |
The standalone verifier is not a registry. It is the reference UltraHonk verifier, deployed on its own to measure the verification cost. A known-good proof verified on it at ledger 3191042, on protocol 27.
| Network | Value |
|---|---|
| Passphrase | Test SDF Network ; September 2015 |
| RPC | https://soroban-testnet.stellar.org |
| Explorer | https://stellar.expert/explorer/testnet |
The contract and the vendored verifier are unaudited. Mainnet Coming soon and an independent security audit Coming soon are listed on Security.
is_covered
is_covered Coming soon is a read for other contracts, such as a lending pool that accepts a token as collateral. Without it, a contract reads get_latest_proof and compares r and l itself.
The interface is read only and follows the pattern of SEP-40, the Stellar oracle interface:
is_covered(issuer, asset)returns the asset's status: covered, short or stale.- Every answer carries the ledger the asset was checked at, so the caller can decide how old a record it accepts.
SEP-40 itself covers price feeds only. Solva follows its read-only design; it does not implement SEP-40.
Next: REST API.