Skip to main content

Epoch Commitments

Status: Live in production since 2026-09-18. Every order event and every fill on Rocky — customer and house alike — is committed to the Canton ledger once per epoch.

On-chain Settlement covers the value layer: a fill that involves a customer account moves assets on Canton as a two-leg DvP. This page covers the data layer. Rocky keeps an append-only journal of everything the venue does — orders placed, orders finished, fills — and, every epoch, writes a commitment to that journal into a Canton contract. Anyone can take a journal row, ask Rocky for its inclusion proof, and check it against the root recorded on the ledger. House-account fills, which are bookkeeping-only in the DvP layer (settlementStatus: "skipped"), are included here in full.

What is committed​

Journal eventWhen it is writtenWhat is committed
spot_order / perp_orderAn order is accepted into the bookRoot only (provable by inclusion proof)
spot_order_done / perp_order_doneAn order is cancelled or fully filledRoot only
spot_fill / perp_fillA fill executesRoot and the full canonical record inside the contract

The journal is written by the exchange database in the same transaction as the business event, so an event cannot exist without its journal row and vice versa. Each row has a global sequence number seq, a canonical text record and a leaf hash sha256(canonical).

Canonical record formats​

Fields are |-separated in a fixed order. Decimals are printed as stored (18 fractional digits); timestamps are UTC with microseconds.

spot_order|<orderId>|<userId>|<symbol>|<side>|<orderType>|<price>|<qty>|<createdAt>
spot_order_done|<orderId>|<FILLED|CANCELED>|<executedQty>|<finalizedAt>
spot_fill|<tradeId>|<symbol>|<takerSide>|<price>|<qty>|<takerUserId>|<makerUserId>|<ts>
perp_order|<orderId>|<userId>|<symbol>|<side>|<price>|<qty>|<createdAt>
perp_order_done|<orderId>|<FILLED|CANCELED>|<executedQty>|<ts>
perp_fill|<tradeId>|<symbol>|<takerSide>|<price>|<qty>|<takerUserId>|<makerUserId>|<ts>

User ids are the exchange's internal account UUIDs. They tie a record to an account without revealing a Canton party; a customer can recognise their own ids from GET /api/v3/myTrades and GET /api/v3/allOrders.

The commitment contract​

Every epoch (60 s in normal operation; the cadence widens to 300 s / 900 s when the venue's Canton traffic budget is under pressure, and it never skips an epoch) Rocky's operator party creates one RockyEpochCommitment contract:

FieldMeaning
epoch_idMonotonic epoch number
seq_from, seq_toThe journal rows committed, inclusive
rootHex SHA-256 Merkle root over the leaves in [seq_from, seq_to], in seq order
prev_rootThe previous epoch's root, linking the commitments into a chain
n_orders, n_cancels, n_fillsCounts by event class
fillsAll fill records of the epoch, newline-joined canonical text
created_atLedger time of the commitment

The tree is a plain binary Merkle tree: leaf = sha256(canonical), internal node = sha256(0x01 ‖ left ‖ right), an odd level duplicates its last node, a single leaf is its own root. The 0x01 prefix keeps a leaf from ever colliding with an internal node.

Because fills is inside the contract, traded volume can be rebuilt from the ledger alone, without trusting Rocky's API. Orders and cancels are only committed through the root: their count does not affect on-chain cost, and each one is still individually provable.

Endpoints​

All are public GET requests on https://api.rocky.exchange, no key required.

EndpointReturns
/v1/chain/statusWhether the anchor is live, the epoch cadence, the signing party, the template id, the current journal backlog and the latest epoch
/v1/chain/epochs?limit=50The most recent epochs (epoch_id, seq_from, seq_to, counts, root, prev_root, status, update_id, contract_id)
/v1/chain/epochs/{id}One epoch
/v1/chain/proof?seq=<n> or ?ref_id=<orderId or tradeId>[&kind=spot_fill]The inclusion proof for one journal row

A proof response:

{
"seq": 52314,
"kind": "spot_fill",
"ref_id": "01a0b2e8-c07a-74a3-bd67-5a9ab2484f0b",
"canonical": "spot_fill|01a0b2e8-c07a-74a3-bd67-5a9ab2484f0b|CETH-CBTC|BUY|0.031990000000000000|0.038000000000000000|e41f4b95-…|0cb01d99-…|2026-09-18T05:02:11.804211Z",
"leaf": "9f1c…",
"index": 1287,
"path": [ { "hash": "3ab0…", "side": "right" }, { "hash": "77e2…", "side": "left" } ],
"epoch": {
"epoch_id": 20, "seq_from": 51001, "seq_to": 53644,
"root": "23077cf0be097cdf2e65c3334fca01e7177cecd439ad0e9c4094cc13eb549411",
"status": "confirmed",
"update_id": "12202f0ad6a8c90d10a8a3b7f2b89217c80e1d0d4b87547beb71c07935a72b4a12a0",
"contract_id": "00c4…"
},
"root_recomputed_matches": true
}

A row whose epoch has not been created yet returns HTTP 202 with the record and leaf but no epoch; ask again after the next epoch.

Public anchors (ccview)​

RockyEpochCommitment is a private contract: only Rocky's operator party is a stakeholder, so the DSO, Scan and public explorers such as ccview cannot see it. To give someone who trusts only public Canton data a foothold, Rocky periodically (every 10 minutes) writes the digest of the newly confirmed epoch roots into the memo of a tiny Canton Coin self-transfer by the operator party. Canton Coin transfers have the DSO as a stakeholder and are indexed by public explorers.

The memo reads rocky:epochs:<from>-<to>:sha256:<digest> where digest = sha256(root_from ‖ root_from+1 ‖ … ‖ root_to) over the raw 32-byte roots in epoch order. GET /v1/chain/anchors lists the anchors with their Canton update id and a ccview_url (the update page on ccview, https://ccview.io/updates/<updateId>/, where the memo appears as splice.lfdecentralizedtrust.org/reason in the TransferFactory_Transfer choice argument); every epoch and proof response carries the public_anchor that covers it.

To verify a fill against ccview alone: take its proof → the epoch root (and check the inclusion path as below) → the anchor covering that epoch → fetch the roots from..to from /v1/chain/epochs, recompute the digest, and compare it with the memo of the Canton Coin transfer shown on ccview.

Verifying a proof yourself​

Do not trust root_recomputed_matches; recompute it. Twelve lines of Python, no dependencies:

import hashlib, json, urllib.request

p = json.load(urllib.request.urlopen(
"https://api.rocky.exchange/v1/chain/proof?ref_id=<tradeId>&kind=spot_fill"))

leaf = hashlib.sha256(p["canonical"].encode()).digest()
assert leaf.hex() == p["leaf"]
acc = leaf
for step in p["path"]:
sib = bytes.fromhex(step["hash"])
acc = hashlib.sha256(b"\x01" + (sib + acc if step["side"] == "left" else acc + sib)).digest()
assert acc.hex() == p["epoch"]["root"]
print("included in epoch", p["epoch"]["epoch_id"], "update", p["epoch"]["update_id"])

Then confirm the root on the ledger: fetch the update by update_id through any participant that hosts an observer of the contract, or ask a Canton block explorer for contract_id, and compare its root field. If the two match, the record was part of the venue's journal at that epoch, and the epoch itself is anchored in Canton's ordering.

What this does and does not guarantee​

  • Completeness and tamper-evidence of the record. Once an epoch is confirmed, no order event or fill in it can be altered or removed without changing a root that is already on the ledger, and the prev_root chain makes a silently rewritten history detectable.
  • Timeliness. An epoch is opened every 60 s and never skipped; a stuck submission is retried under the same command id and blocks newer epochs so the chain stays intact. /v1/chain/status exposes the backlog.
  • Not a settlement. A commitment moves no assets. Delivery of customer fills is the DvP described on On-chain Settlement; house-account fills are internal transfers within Rocky's custody and are committed here as data only.
  • Not an omission proof. Like a proof of reserves, this shows that what is on the ledger was in the journal; that nothing was left out of the journal is guaranteed by the same-transaction database rule and is auditable by comparing n_fills per epoch with the public trade stream.

Cost transparency​

Commitments are paid for from Rocky's own Canton traffic. The venue publishes its traffic budget and consumption per lane at GET /v1/traffic (public), including the L2-epoch lane these contracts land in; GET /v1/traffic/mm is the one-word state Rocky's own market-making fleet follows to stay inside that budget. Measured on 2026-09-18, one commitment costs roughly 3–4 KB of sequencer traffic.