SolvaDocs

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.bin

publish_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:

  1. Requires the owner's authorization with owner.require_auth().
  2. Loads the verifying key. Error 2 if it is missing.
  3. Verifies proof against the key, with the public inputs rebuilt from pub_inputs in the order R, root_h, L, R_prev. A proof that is not exactly 14,592 bytes fails at once. Error 3 if verification fails.
  4. Checks reserves_total >= liabilities_total. Error 4 otherwise.
  5. Checks the growth baseline. On the first proof, prev_reserves must equal reserves_total. After that, it must equal r of the latest stored proof. Error 6 otherwise.
  6. Stores a ProofMeta under the next id, with the ledger timestamp, and sets it as the latest.
  7. Extends the lifetime of the new proof entry and of the contract instance.
  8. 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,
}
FieldMeaning
ProofMeta.root_hRoot hash of the Merkle sum tree, 32 bytes.
ProofMeta.r, ProofMeta.lTotal reserves and total liabilities, integer minor units.
ProofMeta.timestampLedger close time of the publish, Unix seconds.
PathNode.hash, PathNode.sumThe sibling's hash and the total of the sibling's subtree.
PathNode.sibling_is_lefttrue when the sibling is the left child and the current node the right.

Errors

CodeNameMeaning
1AlreadyInitializedThe constructor ran on a contract that already has an owner.
2NotInitializedThe owner, the verifying key or the latest id is missing.
3ProofInvalidThe proof is the wrong length or does not verify against the stored key and the given public inputs.
4InsolventBoundreserves_total is below liabilities_total.
5ProofNotFoundNo proof with that id. Also returned by get_latest_proof before the first publish.
6PrevReservesMismatchprev_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:

contracts/proof-registry/src/storage.rs
#[contractevent]
pub struct ProofPublishedEvent {
    pub reserves_total: u128,
    #[topic]
    pub liabilities_total: u128,
    #[topic]
    pub ts: u64,
}
PartValue
Topic 0The symbol proof_published_event
Topic 1liabilities_total, u128
Topic 2ts, the ledger timestamp, u64
DataA 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

StorageKeyValue
InstanceOwnerAddress
InstanceVkBytes
InstanceLatestIdu64
PersistentProof(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_proof

The 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 testnet

From TypeScript, use the SDK's getOnChainLatestProof and verifyInclusion.

Fees

Measured on Testnet and recorded in contracts/proof-registry/README.md:

TransactionFeeHow often
publish_proofabout 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 constructor210,822 stroops (0.0210822 XLM)once per registry
Wasm upload16,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.

ContractAddress
Kestrel Anchor registryCAFUK25F56VW442VAOHDKGMPYF2BZ3ERVLVAM4H6JTN2WGZPDUQLRTPN
Solstice Exchange registryCDSQ35U2Y3VC44PJC6RRUJVCES2DLBWELDN32AIAEY4J35H4WBEKEUQW
Harbor Wallet registryCBOA5AJEMTQ5CSMHE6WFWWBIYYMNSUQGKWE6FCFB2KKNJSJPHBWSCSJK
Standalone verifierCA3KQYYAGUEIURJSB5CSWVMNIY2FF2EG3YTM7SBBRSN7DQWZXCCXWXN3

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.

NetworkValue
PassphraseTest SDF Network ; September 2015
RPChttps://soroban-testnet.stellar.org
Explorerhttps://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.

On this page