Skip to main content

Usage proofs

CoRelayer counts consumption off chain — the contract never sees a relayed transaction. So the obvious question is how anyone outside can check what was counted. The answer is a commitment: every settlement batch writes a Merkle root over every ledger row it covers into the contract, and a single row can be proven against it.

What is committed, and where​

The reporter settles usage in batches with one contract call:

settleUsage(batch_id, window_end_ms, usage_root, totals, lines)
ArgumentWhat it is
batch_idStrictly the previous batch id plus one. Exactly one batch per id can ever execute.
window_end_msThe watermark: every relayed transaction executed at a block timestamp at or before it is in the ledger.
usage_rootThe Merkle root over all ledger rows of the window — cap, pay-as-you-go, free and internal alike.
totalsRow and Relay Unit counts, informational. The contract trusts only the debits it computes itself.
lines19-byte lines that debit pay-as-you-go escrow: an account id, a period, a sequence number and a unit count. No price and no amount — the contract prices each line from its own tariff history.

Each accepted batch emits usageSettled carrying the root, the window, the counts, and the usage chain head:

usage_chain_head = sha256(usage_chain_head ‖ batch_id ‖ window_end_ms ‖ usage_root ‖ totals)

a rolling on-chain commitment to the whole audit history. A backend that wanted to rewrite an old batch after the fact would have to produce a different chain head, and the chain already holds the real one.

How a row becomes a leaf​

The tree is built RFC 6962-style, with domain separation between leaves (0x00) and interior nodes (0x01), over the rows sorted by (executed_block_ts_ms, tx_hash). Rows discovered late for an already committed window — orphan recovery, an anticipation row whose billing was decided later — follow, flagged late.

leaf = sha256( 0x00
‖ tx_hash 32 bytes
‖ account 32 bytes — the public key the row was billed to
‖ sender 32 bytes
‖ relayer 32 bytes
‖ executed_block_ts_ms u64, big-endian
‖ ru u32
‖ ru_cap u32
‖ ru_payg u32
‖ period_id u32
‖ tariff_idx u16
‖ billing_class u8
‖ ru_schedule_version u16
‖ late u8 )

node = sha256( 0x01 ‖ left ‖ right )

Everything in a leaf is either a chain fact — the hash, the addresses, the block timestamp — or a number anyone can recompute from chain facts: ru follows from the transaction's own fields by the Relay Unit formula of the recorded schedule version. A Merkle root rather than a plain hash of the batch costs the same 32 bytes on chain and is what makes a per-row proof possible. SHA-256 was chosen because every client language has it.

What a proof would show​

For one billed transaction, a proof ties three things together:

  1. the leaf of that transaction, recomputed by you from chain facts;
  2. an audit path of sibling hashes from that leaf to a root;
  3. the root in the usageSettled event of a specific settleUsage transaction on chain.

If the recomputed root equals the committed one, the row was counted exactly as the leaf says — including which account paid and how many units it cost.

The route, and what it returns today​

GET /v1/usage/{txHash}/proof
Authorization: Bearer <native-auth token of the billed account>

Private to the billed account: native-auth, or a sponsor key with the read scope. The response is a UsageProof: txHash, batchId, windowEndMs, usageRoot, usageChainHead, settleTxHash, leafHash, leaf (the leaf's members) and path (the audit path as { hash, side } steps). A row that has not been settled yet answers NOT_FOUND: until a batch commits a root, there is nothing to prove against.

Not verifiable end to end yet

As built, the route returns the committed root and the leaf hash, but an empty audit path, and its leaf object carries only part of the preimage (the unit counts, the period and the block timestamp — not the addresses, the billing class or the schedule version). The interior nodes of a batch's tree are recomputed only by the settler, and the route does not serve them yet. Until it does, a proof can be checked only for a batch whose tree has a single leaf.

The changelog says when the route serves complete paths.

What you can already rely on:

  • the construction above, which is what the backend's ledger implements and tests — leaf hashing, root and path computation, and the chain head;
  • the on-chain side, which is fixed by the contract: usage_root and usage_chain_head in every usageSettled event, and batch_id strictly sequential;
  • the rule that a row is only ever billed once, keyed by (sender, nonce), with the executed hash winning. (Delivery guarantees)

Reading the settlement state​

getSettlementStateThe reporter, whether settlement is paused, and the settlement state and totals.
usageSettledOne per accepted batch: root, chain head, window, counts, debits.
paygSettledOne per accepted pay-as-you-go line.
settleLineRejectedA line the contract refused, with its reason. A refused line does not advance the account's sequence.

The reporter key can settle usage and nothing else, bounded by each account's escrow and by a per-window cap, and the contract rejects a line whose units exceed what the account's rate class could have sent in the elapsed time. (The contract)