StateCommitment
StateCommitment is the contract that turns "trust our numbers" into "verify our
numbers." The Senticore matching engine runs off-chain in deterministic Rust, but
its output is only trustworthy if it is anchored to something no operator can
quietly rewrite. That anchor is a checkpoint: a signed, monotonic commitment
published on-chain that advances the checkpoint state root and, in the same
transaction, publishes the Merkle roots the vault
uses to authorize withdrawals and unilateral exits.
Every checkpoint must carry a 7-of-10 publisher quorum of ECDSA signatures over an EIP-712 typed-data hash. No single operator can advance state, and no operator can move funds out from under you: withdrawals settle against roots that a quorum put on-chain, and anyone can recompute the signing hash and re-verify the signatures independently.
StateCommitment is live on Arbitrum One at
0xf88424085805b55ac6ad584aa9bb8e1f8eb6cf89.
This is a capped private-beta deployment, not a public launch. At the
2026-09-04 review latestCheckpointId() returned 1005, requireExitRoot() was
true, the contract was unpaused, and owner() resolved to the
TimelockController (0x4e2b…9162). Dynamic chain state and the
Addresses page override copied checkpoint values below.
Why this design matters
For a venue holding real collateral, the question a market maker or institution
actually asks is: "If the operator turns hostile or gets compromised, can I still
get my funds, and can I prove the books balance?" StateCommitment is engineered
so the answer is yes, for structural reasons rather than promises:
- State cannot advance on one key. A checkpoint needs 7 independent publisher signatures. Compromising fewer than 7 keys cannot forge state; losing up to 3 keys does not halt the venue.
- Withdrawals are proof-backed, not operator-gated. Each checkpoint publishes a
withdrawRoot. The vault only releases funds against a Merkle proof into a root that a quorum committed — see MultiCollateralVault. - Solvency is auditable. Each checkpoint publishes a
solvencyRootbinding the liability set, so protocol- and account-level solvency proofs verify against on-chain data. - Exit is unilateral. Once the one-way
requireExitRootswitch is on, every checkpoint must publish a freshexitRoot, guaranteeing users a standing withdraw-without-cooperation path. - The off-chain log is reproducible. A data-availability (DA) commitment binds the exact off-chain data bundle behind each checkpoint, so validators can replay the event log and independently derive the market-state, solvency, and exit roots.
Constants
| Constant | Value | Purpose |
|---|---|---|
PUBLISHER_SET_SIZE | 10 | Fixed number of publisher slots. |
PUBLISHER_QUORUM | 7 | Minimum valid signatures per checkpoint. |
GOVERNANCE_UPDATE_DELAY | 48 hours | Timelock for guardian / emergency-council / publisher-set rotations. |
PUBLISHER_SET_UPDATE_DELAY | 48 hours | Alias of GOVERNANCE_UPDATE_DELAY for standard publisher rotation. |
EMERGENCY_PUBLISHER_SET_DELAY | 8 hours | Shorter timelock for emergency publisher rotation. |
EMERGENCY_PUBLISHER_SET_COOLDOWN | 30 days | Minimum spacing between emergency rotations. |
Why 7-of-10? A quorum of 7 means an attacker must compromise a majority of
independent signing keys to forge a checkpoint — far harder than a single hot key.
A ceiling of 10 and a floor of 7 means the venue keeps publishing even if up to 3
publishers are offline or revoked, so security does not come at the cost of
liveness. A commit accepts between PUBLISHER_QUORUM and PUBLISHER_SET_SIZE
signatures inclusive (7 to 10); fewer than 7 or more than 10 reverts.
The checkpoint
A checkpoint is submitted as a CheckpointInput struct plus an array of
publisher signatures. It describes a contiguous range of local batches, chains one
state root to the next, and carries every root and context hash a verifier needs.
Checkpoint fields (CheckpointInput)
| Field | Type | Meaning |
|---|---|---|
checkpointId | uint256 | Monotonic checkpoint counter; must be latest + 1 (genesis is 1). |
fromLocalBatchId / toLocalBatchId | uint256 | Inclusive local-batch range; must be contiguous with the previous checkpoint. |
fromLocalEndLsn / toLocalEndLsn | uint64 | Durable WAL log-sequence-number range; strictly increasing across checkpoints. |
prevStateRoot | bytes32 | Must equal the last committed newStateRoot (or genesisStateRoot at genesis). |
newStateRoot | bytes32 | New checkpoint state root after applying this range. Under the current private-beta validator policy this is the market/order-book root; financial balances are bound separately by solvencyRoot and exitRoot. Non-zero. |
rangeHash | bytes32 | Commitment over the batch range's contents. Non-zero. |
daPointer | string | Human/URI pointer to the off-chain data bundle for this checkpoint. |
daPointerHash | bytes32 | DA commitment hash binding daPointer + manifest + bundle + schema version. |
daManifestHash | bytes32 | Hash of the DA bundle manifest. Non-zero. |
daBundleHash | bytes32 | Hash of the DA bundle payload. Non-zero. |
daCommitmentSchemaVersion | uint64 | DA commitment schema version. Non-zero. |
withdrawRoot | bytes32 | Root the vault uses for withdrawal claim proofs. Non-zero. |
solvencyRoot | bytes32 | Root for liabilities / audit proofs. Non-zero. |
validatorSetVersion | uint64 | Settlement context: engine validator-set version. Non-zero. |
validationPolicyHash | bytes32 | Settlement context: active validation policy. Non-zero. |
engineBuildHash | bytes32 | Settlement context: matching-engine build identity. Non-zero. |
configHash | bytes32 | Settlement context: runtime config identity. Non-zero. |
The optional exitRoot is not part of CheckpointInput; it is passed as a separate
argument to commitCheckpointWithExitRoot and folded into the signed hash.
Strict monotonic validation
Checkpoints form an unbroken chain. _validateCheckpoint enforces:
- Genesis (
latestCheckpointId == 0):checkpointId == 1,fromLocalBatchId == 1,prevStateRoot == genesisStateRoot, andtoLocalEndLsn != 0. - Every subsequent checkpoint:
checkpointId == latestCheckpointId + 1,fromLocalBatchId == latestBatchId + 1(contiguous, no gaps or overlaps),prevStateRoot == latestStateRoot(unbroken hash chain), andtoLocalEndLsn > latestCheckpointEndLsn(LSN strictly increases). - Range sanity: ids and batch ids non-zero,
toLocalBatchId >= fromLocalBatchId,toLocalEndLsn >= fromLocalEndLsn, andrangeHash/newStateRootnon-zero. - Root sanity:
withdrawRoot,solvencyRoot, all four settlement-context fields, and all DA fields non-zero, with the DA commitment self-consistent (below).
A duplicate checkpointId reverts CommitAlreadyExists. Because the chain is
strict, there is no way to skip, reorder, or fork a checkpoint on-chain without
producing a break any verifier can detect.
Four kinds of roots
A single checkpoint commits four distinct roots, each consumed by a different verifier. Keeping them separate lets each proof system evolve independently while all four are anchored by the same quorum signature.
| Root | Getter | Consumed by | What it proves |
|---|---|---|---|
| State root | latestStateRoot, event log | Indexers / replayers | The canonical market and order-book state under the current validator policy, chained across checkpoints and reproducible by replay. |
withdrawRoot | getWithdrawRoot(localBatchId), latestWithdrawRoot | Vault executeWithdrawWithClaim | A user's withdrawal claim is authorized for a given local batch. |
solvencyRoot | getSolvencyRoot(localBatchId), latestSolvencyRoot | Solvency / audit proofs | The liability set behind the venue's reserves. |
exitRoot | getExitRoot(localBatchId), latestExitRoot | Vault unilateral exit | Account-and-asset exit entitlements, including native token units and vault/token bindings. Present only when committed via commitCheckpointWithExitRoot. |
The matching engine can calculate a full internal state root that includes its
ledger. The deployed settlement validators intentionally submit
market_only_commit_root as newStateRoot, then independently recompute the
financial ledger into solvencyRoot and exitRoot. A deposit or withdrawal can
therefore leave latestStateRoot unchanged while correctly changing the financial
roots. Do not use an unchanged state root as evidence that a balance update was
missed; verify the relevant account projection and the checkpoint's solvency/exit
proof material instead. The validator policy and build/config hashes bind this
interpretation to the checkpoint.
The withdraw, solvency, and exit roots are indexed per local batch
(toLocalBatchId), so the vault can look up the exact root for the batch a claim
references rather than only the latest.
EIP-712 signing
Publishers sign a typed-data hash, not raw bytes, so the payload is human-readable and domain-bound. The EIP-712 domain is:
- name:
SenticoreStateCommitment - version:
1 - chainId: current chain (prevents cross-chain replay)
- verifyingContract: this contract's address
There are two struct typehashes:
CheckpointCommitment(uint256 checkpointId,uint256 fromLocalBatchId,uint256 toLocalBatchId,uint64 fromLocalEndLsn,uint64 toLocalEndLsn,bytes32 rootsHash,bytes32 settlementContextHash)CheckpointWithExitRootCommitment(...,bytes32 rootsHash,bytes32 exitRoot,bytes32 settlementContextHash)
rootsHash is keccak256 over prevStateRoot, newStateRoot, rangeHash,
daPointerHash, withdrawRoot, solvencyRoot (and exitRoot in the exit-root
variant). settlementContextHash is keccak256 over validatorSetVersion,
validationPolicyHash, engineBuildHash, configHash. Folding these into
sub-hashes keeps the top-level struct compact while still binding every field.
Anyone can reproduce the exact bytes a publisher signed using the public helpers, which is what makes signature verification independent of the operator:
| Helper | Returns |
|---|---|
domainSeparator() | The EIP-712 domain separator for this contract/chain. |
hashCheckpoint(input) | The struct hash (no domain). |
checkpointSigningHash(input) | The full EIP-712 digest publishers sign. |
hashCheckpointWithExitRoot(input, exitRoot) | Struct hash for the exit-root variant. |
checkpointWithExitRootSigningHash(input, exitRoot) | Full digest for the exit-root variant. |
Quorum enforcement
On commit, _requirePublisherQuorumHash recovers a signer from each signature via
ECDSA.recover against the EIP-712 digest and requires:
- the signature count is between 7 and 10 inclusive,
- every recovered signer is an active publisher (
isPublisher[signer]), elseNotPublisher, - no signer appears twice (duplicates revert
NotPublisher).
So 7 distinct, currently-active publishers must sign the exact checkpoint. A signature over a different checkpoint, a different exit root, or a different chain does not recover to a valid publisher and is rejected.
Data-availability commitment
A state root is only meaningful if the data behind it is retrievable and
reproducible. Each checkpoint binds its off-chain data bundle through a DA
commitment under the domain SENTICORE_DA_COMMITMENT_V1. The stored daPointerHash
is keccak256(abi.encode(DA_COMMITMENT_DOMAIN_V1, daPointer, daManifestHash, daBundleHash, daCommitmentSchemaVersion)),
and the commit reverts unless the submitted daPointerHash matches that
recomputation — you cannot commit a state root while pointing at a bundle that does
not hash to it.
Verification helpers:
| Function | Purpose |
|---|---|
computeDaCommitmentHash(daPointer, daManifestHash, daBundleHash, schemaVersion) | Recompute the DA commitment hash off-chain. |
verifyDaCommitment(checkpointId, daPointer, daManifestHash, daBundleHash, schemaVersion) | Check a full DA tuple against a stored checkpoint. |
computeDaPointerHash(daPointer) | Plain keccak256(bytes(daPointer)) helper. |
verifyDaPointer(checkpointId, daPointer) | Check that a daPointer reproduces the stored commitment. |
Commit entry points
| Function | Exit root | Notes |
|---|---|---|
commitCheckpoint(input, signatures) | None | Reverts LegacyFlowDisabled once requireExitRoot is enabled. |
commitCheckpointWithExitRoot(input, exitRoot, signatures) | Required | exitRoot must be non-zero and must differ from withdrawRoot. |
Both are whenNotPaused: while the contract is paused, no checkpoint can be
committed. setRequireExitRoot(true) is a one-way, owner-only switch — once on
it cannot be turned off, and every future checkpoint must carry an exit root. This
is the on-chain guarantee that a fresh exit tree always exists, so unilateral exit
can never silently lapse.
Several older entry points now revert LegacyFlowDisabled and exist only for ABI
compatibility: commitCheckpointHashOnly, commitCheckpointHashOnlyWithWithdrawRoot,
commitCheckpointHashOnlyWithRoots, setPublisher, grantPublisher,
revokePublisher, and setGenesisStateRoot. Publishers are not managed
individually — the only way to change the publisher set is the timelocked
set-rotation flow below. The genesis root is immutable after deployment.
Governance and safety machinery
Institutional readers care most about what happens when something goes wrong. The contract layers Ownable2Step ownership, a guardian, and an emergency council, with timelocks sized to the severity of each action.
Roles
| Role | Powers |
|---|---|
Owner (Ownable2Step) | Schedule publisher-set rotations, set guardian / emergency council, setRequireExitRoot, unpause. transferOwnership requires the new owner to be a contract (code.length > 0); renounceOwnership is disabled. |
| Guardian | pause, cancel a pending standard publisher rotation, cancel guardian/council updates, emergency-revoke a publisher. |
| Emergency Council | Schedule an emergency publisher rotation (shorter delay, auto-pause), emergency-revoke a publisher, cancel emergency actions. |
Ownership transfer is two-step (propose then accept) and the new owner must be a
contract — this blocks fat-fingering ownership to an EOA or the zero address, and
renounceOwnership() reverting means the venue can never end up ownerless.
Timelocked rotations
Standard publisher-set rotation is a 48-hour timelock so the ecosystem has time to react before signing power changes:
schedulePublisherSetUpdate(newSet) // owner; validates the 10-address set
│ wait GOVERNANCE_UPDATE_DELAY (48h)
▼
activatePublisherSetUpdate() // permissionless crank after the window
Guardian and emergency-council changes follow the same 48-hour
schedule → wait → activate pattern (the first time a guardian or council is set it
takes effect immediately; subsequent changes are timelocked). cancelPublisherSetUpdate
(guardian or owner) can abort a pending rotation during the window. New publisher
sets are validated for exactly 10 non-zero, non-placeholder, distinct addresses.
Emergency publisher rotation
If keys are actively compromised, waiting 48 hours may be unacceptable.
scheduleEmergencyPublisherSetUpdate (emergency council only) uses the shorter
EMERGENCY_PUBLISHER_SET_DELAY of 8 hours, auto-pauses the contract, and
auto-cancels any pending standard rotation. It is rate-limited by the
EMERGENCY_PUBLISHER_SET_COOLDOWN of 30 days so the fast path cannot be abused as a
routine tool. During the 8-hour window the guardian or owner can cancel it — so
the fast path is powerful but still checkable by another role before it lands.
For a single bad key, emergencyRevokePublisherAndPause (council, guardian, or
owner) revokes one publisher and pauses in a single call — but only while the active
publisher count stays above quorum, so the venue can never be dropped below a
functioning 7-of-10 by a revoke.
Pause
pause() (guardian or owner) blocks all commits; unpause() is owner-only. Pausing
is intentionally easier than unpausing: any guardian can stop the world, but only
the owner can restart it.
Verification path
A user, market maker, or indexer verifies a withdrawal or exit end-to-end without trusting the operator:
Concretely:
- The checkpoint exists on-chain —
getCheckpointCommit(checkpointId)returns a commit with a non-zerocommittedAt(elseCommitNotFound). - It carries a valid quorum — recompute
checkpointSigningHash(input)(or the exit-root variant) and confirm at least 7 distinct signatures recover to active publishers ingetPublisherSet(). - The proof matches the committed root — build a Merkle proof into
getWithdrawRoot(batchId)(orgetExitRoot(batchId)for a unilateral exit). - The vault accepts it —
executeWithdrawWithClaimin MultiCollateralVault re-checks the proof against the same committed root and releases funds.
The DA commitment closes the loop: verifyDaCommitment confirms the off-chain
bundle behind the checkpoint is the one that hashes to the committed data, so the
whole ledger can be replayed and re-derived independently.
Events
Indexers reconstruct state and monitor governance from these events:
| Event | Emitted when |
|---|---|
CheckpointCommitted | A checkpoint commits (full range, roots, DA pointer). |
CheckpointSettlementContextCommitted | Per checkpoint: validator-set version and policy/build/config hashes. |
CheckpointDaCommitmentCommitted | Per checkpoint: DA pointer, manifest, bundle, schema version. |
CheckpointExitRootCommitted | An exit root is committed for a local batch. |
CheckpointQuorumVerified | Signatures verified (checkpoint hash, submitter, signature count). |
BatchCommitted | Legacy-compatible mirror for downstream indexers. |
PublisherSetUpdateScheduled / Cancelled / PublisherSetActivated | Standard publisher rotation lifecycle. |
EmergencyPublisherSetUpdateScheduled / Cancelled / EmergencyPublisherSetActivated | Emergency rotation lifecycle. |
PublisherWhitelisted / PublisherEmergencyRevoked | Individual publisher activation / emergency revoke. |
GuardianSet / Activated / EmergencyCouncil* | Role change lifecycle. |
ExitRootRequirementSet | The one-way exit-root requirement is enabled. |
Test surface
StateCommitment behavior is exercised by dedicated Foundry suites (see
test/StateCommitment.t.sol, test/StateCommitment_Emergency.t.sol, and
onchain/test/StateCommitment/Invariants.t.sol), covering: quorum thresholds (6 rejected,
7/10 accepted, 11 rejected), duplicate and non-publisher signatures, EIP-712
cross-chain replay rejection, exit-root commit paths (zero/withdraw-root-alias
rejection, wrong-exit-root signatures), the one-way requireExitRoot switch,
genesis and contiguity/monotonicity rules, DA and settlement-context validation,
48-hour standard rotation, 8-hour emergency rotation with 30-day cooldown and
auto-pause, guardian/owner cancellation of emergency actions, single-publisher
emergency revoke, and ownership-transfer constraints. End-to-end settlement against
the vault is covered under onchain/test/FullStack/SettlementRoundTrip.t.sol.
See also
- MultiCollateralVault — consumes the withdraw and exit roots for proof-backed withdrawals and unilateral exits.
- Addresses — verified live private-beta deployments.
- Trust model — what you must trust vs. verify.
- Architecture — how the engine, sequencer, and contracts fit together.