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)
| Argument | What it is |
|---|---|
batch_id | Strictly the previous batch id plus one. Exactly one batch per id can ever execute. |
window_end_ms | The watermark: every relayed transaction executed at a block timestamp at or before it is in the ledger. |
usage_root | The Merkle root over all ledger rows of the window — cap, pay-as-you-go, free and internal alike. |
totals | Row and Relay Unit counts, informational. The contract trusts only the debits it computes itself. |
lines | 19-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:
- the leaf of that transaction, recomputed by you from chain facts;
- an audit path of sibling hashes from that leaf to a root;
- the root in the
usageSettledevent of a specificsettleUsagetransaction 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.
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_rootandusage_chain_headin everyusageSettledevent, andbatch_idstrictly 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
getSettlementState | The reporter, whether settlement is paused, and the settlement state and totals. |
usageSettled | One per accepted batch: root, chain head, window, counts, debits. |
paygSettled | One per accepted pay-as-you-go line. |
settleLineRejected | A 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)