Epoch lifecycle

The states an epoch moves through, the exact condition for each transition, and which committed orders are allowed into the auction.

Open30 sRelay accepts sealed orders
Commit window15 sRoot must be anchored. Key does not exist yet
Settle window300 sDecrypt, match, publish, settle
  1. openTime
  2. closeTime (cutoff)
  3. decryptTime (drand round)
  4. settleDeadline
The mainnet schedule: 30 seconds open, 15 seconds between the cutoff and decryption, 300 seconds to settle. Widths are not to scale. The next epoch opens at this one's cutoff.

5. Epoch state machine#

  1. SCHEDULEDtime
  2. OPENtime
  3. CLOSEDcommit()
  4. COMMITTEDtime
  5. DECRYPTABLEpostMatch()
  6. MATCHEDevery market settled
  7. SETTLED

CLOSED with no commit by decryptTime, anything unsettled after settleDeadline, or cancelEpoch() lead to CANCELLED

States in KasumiEpochManager. Time-driven transitions cost no gas. An epoch that misses its commit window, or its settle deadline, reads CANCELLED.

Epoch timing comes from a schedule: startTime, epochDuration, revealDelay, settleWindow. For epoch n:

text
openTime       = startTime + (n - firstEpochId) * epochDuration
closeTime      = openTime + epochDuration
decryptTime    = closeTime + revealDelay
settleDeadline = decryptTime + settleWindow

The duration is not hardcoded. reconfigure changes all three intervals from two epochs ahead, so no open or closing epoch is altered. A second change cannot be queued until the first is in effect.

KasumiEpochManager.status(epochId) derives the state. Time-driven states need no transaction, so an empty epoch costs no gas.

State Condition
SCHEDULED now < openTime
OPEN openTime <= now < closeTime
CLOSED closeTime <= now < decryptTime, no root
COMMITTED root anchored, now < decryptTime
DECRYPTABLE root anchored, decryptTime <= now <= settleDeadline, no result published
MATCHED result published, now <= settleDeadline, not every published market settled
SETTLED every published market settled with exactly its published hash. Terminal.
CANCELLED no root by decryptTime, or not settled by settleDeadline, or cancelEpoch. Terminal.

Transactions:

  • commit(epochId, root, orderCount): relay only, only in CLOSED. Root and count must be non-zero. This is the rule that matters: a commitment can only be anchored between the order cutoff and the decryption time. It cannot be replaced.
  • triggerDecryption(epochId): anyone, in DECRYPTABLE or MATCHED. Emits DecryptionTriggered once. It exists for event-based key release. Nothing in v1 consumes it.
  • postMatch(epochId, marketIds, marketHashes): matcher only, only in DECRYPTABLE, once. Publishes one hash per market before anything settles. Market ids must be strictly ascending and hashes non-zero. The contract stores each hash in publishedMarketHash and stores resultHash = keccak256(u256(epochId) ‖ u256(n) ‖ marketHashes). An empty result (no market crossed) moves the epoch straight to SETTLED.
  • amendMarket(epochId, marketId, newHash): matcher only, only in MATCHED, only for a published market that has not settled. Replaces its hash, or withdraws the market with newHash = 0. Emits MarketAmended(epochId, marketId, oldHash, newHash). resultHash keeps the original publication, so every amendment is visible to anyone comparing the two. It exists so the matcher can re-match one market after a participant makes its settlement revert (section 7). A market cannot be added this way.
  • markMarketSettled(epochId, marketId, marketHash): settlement contract only, only in MATCHED. Reverts with ResultHashMismatch unless marketHash equals the published hash for that market, and with MarketAlreadySettled on a repeat. When the number of settled markets equals the number of published markets the epoch becomes SETTLED. There is no separate finalisation call.
  • cancelEpoch(epochId): owner only, any state except SETTLED and CANCELLED.

6. Opening an epoch#

openEpoch in packages/sdk/src/pipeline.ts decrypts the committed batch and decides which orders enter the auction. Entries are processed in sequence order. An entry is rejected with the first reason that applies:

# Code Condition
1 UNDECRYPTABLE the scheme cannot decrypt the ciphertext
2 MALFORMED the plaintext is not a valid sealed payload
3 INVALID_ORDER structural check fails: side, zero or oversized amount or price, minFillBase > baseAmount, empty validity window, deviation or slippage out of range, base equals quote
4 COMMITMENT_MISMATCH orderCommitment(order) differs from the envelope's commitment
5 WRONG_EPOCH order.epochId is not this epoch
6 OUTSIDE_VALIDITY validAfter > decryptTime or validUntil < settleDeadline
7 UNKNOWN_MARKET the token pair is not an allowlisted market
8 BAD_SIGNATURE the signer is not owner (ECDSA, or ERC-1271 if a contract check is supplied)
9 CANCELLED a cancel secret was recorded and hashes to the payload's cancelHash
10 NONCE_USED the nonce is spent onchain, or an earlier entry of the same owner used it
11 INSUFFICIENT_FUNDS min(balance, allowance) does not cover this order plus the owner's earlier accepted orders in the same token
12 TRANSFER_BLOCKED the chain view reports that a token leg of this order cannot move: the owner or receiver is on the Stock Token registry's blocklist. Only checked when the operator supplies the hook; the simulation does not.

Rule 6 requires an order to stay valid for the whole settlement window. An order can therefore never be used to revert a batch by expiring between matching and settlement.

Sequence matters only for rules 10 and 11, to pick which of two conflicting orders from the same wallet survives. It never affects price or allocation.

Accepted orders go to the auction, specified in MATCHING.md.