Epoch lifecycle
The states an epoch moves through, the exact condition for each transition, and which committed orders are allowed into the auction.
openTimecloseTime (cutoff)decryptTime (drand round)settleDeadline
5. Epoch state machine#
SCHEDULEDtimeOPENtimeCLOSEDcommit()COMMITTEDtimeDECRYPTABLEpostMatch()MATCHEDevery market settledSETTLED
CLOSED with no commit by decryptTime, anything unsettled after settleDeadline, or cancelEpoch() lead to CANCELLED
Epoch timing comes from a schedule: startTime, epochDuration, revealDelay, settleWindow. For
epoch n:
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 inCLOSED. 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, inDECRYPTABLEorMATCHED. EmitsDecryptionTriggeredonce. It exists for event-based key release. Nothing in v1 consumes it.postMatch(epochId, marketIds, marketHashes): matcher only, only inDECRYPTABLE, once. Publishes one hash per market before anything settles. Market ids must be strictly ascending and hashes non-zero. The contract stores each hash inpublishedMarketHashand storesresultHash = keccak256(u256(epochId) ‖ u256(n) ‖ marketHashes). An empty result (no market crossed) moves the epoch straight toSETTLED.amendMarket(epochId, marketId, newHash): matcher only, only inMATCHED, only for a published market that has not settled. Replaces its hash, or withdraws the market withnewHash = 0. EmitsMarketAmended(epochId, marketId, oldHash, newHash).resultHashkeeps 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 inMATCHED. Reverts withResultHashMismatchunlessmarketHashequals the published hash for that market, and withMarketAlreadySettledon a repeat. When the number of settled markets equals the number of published markets the epoch becomesSETTLED. There is no separate finalisation call.cancelEpoch(epochId): owner only, any state exceptSETTLEDandCANCELLED.
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.