Skip to main content

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 canonical 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.

Beta status

Senticore is in capped private beta. No mainnet deployment exists yet, so this page documents contract behavior, not a live address. When contracts are deployed, addresses land on the Addresses page. Everything below is drawn directly from contracts/src/StateCommitment.sol and IStateCommitment.sol.

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 solvencyRoot binding the liability set, so protocol- and account-level solvency proofs verify against on-chain data.
  • Exit is unilateral. Once the one-way requireExitRoot switch is on, every checkpoint must publish a fresh exitRoot, 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 anyone can replay the event log and re-derive the committed state root byte-for-byte.

Constants

ConstantValuePurpose
PUBLISHER_SET_SIZE10Fixed number of publisher slots.
PUBLISHER_QUORUM7Minimum valid signatures per checkpoint.
GOVERNANCE_UPDATE_DELAY48 hoursTimelock for guardian / emergency-council / publisher-set rotations.
PUBLISHER_SET_UPDATE_DELAY48 hoursAlias of GOVERNANCE_UPDATE_DELAY for standard publisher rotation.
EMERGENCY_PUBLISHER_SET_DELAY8 hoursShorter timelock for emergency publisher rotation.
EMERGENCY_PUBLISHER_SET_COOLDOWN30 daysMinimum 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)

FieldTypeMeaning
checkpointIduint256Monotonic checkpoint counter; must be latest + 1 (genesis is 1).
fromLocalBatchId / toLocalBatchIduint256Inclusive local-batch range; must be contiguous with the previous checkpoint.
fromLocalEndLsn / toLocalEndLsnuint64Durable WAL log-sequence-number range; strictly increasing across checkpoints.
prevStateRootbytes32Must equal the last committed newStateRoot (or genesisStateRoot at genesis).
newStateRootbytes32New canonical ledger root after applying this range. Non-zero.
rangeHashbytes32Commitment over the batch range's contents. Non-zero.
daPointerstringHuman/URI pointer to the off-chain data bundle for this checkpoint.
daPointerHashbytes32DA commitment hash binding daPointer + manifest + bundle + schema version.
daManifestHashbytes32Hash of the DA bundle manifest. Non-zero.
daBundleHashbytes32Hash of the DA bundle payload. Non-zero.
daCommitmentSchemaVersionuint64DA commitment schema version. Non-zero.
withdrawRootbytes32Root the vault uses for withdrawal claim proofs. Non-zero.
solvencyRootbytes32Root for liabilities / audit proofs. Non-zero.
validatorSetVersionuint64Settlement context: engine validator-set version. Non-zero.
validationPolicyHashbytes32Settlement context: active validation policy. Non-zero.
engineBuildHashbytes32Settlement context: matching-engine build identity. Non-zero.
configHashbytes32Settlement 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, and toLocalEndLsn != 0.
  • Every subsequent checkpoint: checkpointId == latestCheckpointId + 1, fromLocalBatchId == latestBatchId + 1 (contiguous, no gaps or overlaps), prevStateRoot == latestStateRoot (unbroken hash chain), and toLocalEndLsn > latestCheckpointEndLsn (LSN strictly increases).
  • Range sanity: ids and batch ids non-zero, toLocalBatchId >= fromLocalBatchId, toLocalEndLsn >= fromLocalEndLsn, and rangeHash / newStateRoot non-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.

RootGetterConsumed byWhat it proves
State rootlatestStateRoot, event logIndexers / replayersThe canonical ledger, chained across checkpoints and reproducible by replaying the published events.
withdrawRootgetWithdrawRoot(localBatchId), latestWithdrawRootVault executeWithdrawWithClaimA user's withdrawal claim is authorized for a given local batch.
solvencyRootgetSolvencyRoot(localBatchId), latestSolvencyRootSolvency / audit proofsThe liability set behind the venue's reserves.
exitRootgetExitRoot(localBatchId), latestExitRootVault unilateral exitA user can withdraw without operator cooperation. Present only when committed via commitCheckpointWithExitRoot.

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:

HelperReturns
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]), else NotPublisher,
  • 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:

FunctionPurpose
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

FunctionExit rootNotes
commitCheckpoint(input, signatures)NoneReverts LegacyFlowDisabled once requireExitRoot is enabled.
commitCheckpointWithExitRoot(input, exitRoot, signatures)RequiredexitRoot 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.

Legacy flows are disabled

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

RolePowers
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.
Guardianpause, cancel a pending standard publisher rotation, cancel guardian/council updates, emergency-revoke a publisher.
Emergency CouncilSchedule 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:

  1. The checkpoint exists on-chaingetCheckpointCommit(checkpointId) returns a commit with a non-zero committedAt (else CommitNotFound).
  2. It carries a valid quorum — recompute checkpointSigningHash(input) (or the exit-root variant) and confirm at least 7 distinct signatures recover to active publishers in getPublisherSet().
  3. The proof matches the committed root — build a Merkle proof into getWithdrawRoot(batchId) (or getExitRoot(batchId) for a unilateral exit).
  4. The vault accepts itexecuteWithdrawWithClaim in 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:

EventEmitted when
CheckpointCommittedA checkpoint commits (full range, roots, DA pointer).
CheckpointSettlementContextCommittedPer checkpoint: validator-set version and policy/build/config hashes.
CheckpointDaCommitmentCommittedPer checkpoint: DA pointer, manifest, bundle, schema version.
CheckpointExitRootCommittedAn exit root is committed for a local batch.
CheckpointQuorumVerifiedSignatures verified (checkpoint hash, submitter, signature count).
BatchCommittedLegacy-compatible mirror for downstream indexers.
PublisherSetUpdateScheduled / Cancelled / PublisherSetActivatedStandard publisher rotation lifecycle.
EmergencyPublisherSetUpdateScheduled / Cancelled / EmergencyPublisherSetActivatedEmergency rotation lifecycle.
PublisherWhitelisted / PublisherEmergencyRevokedIndividual publisher activation / emergency revoke.
GuardianSet / Activated / EmergencyCouncil*Role change lifecycle.
ExitRootRequirementSetThe 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 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 test/FullStack/SettlementRoundTrip.t.sol.

See also

  • MultiCollateralVault — consumes the withdraw and exit roots for proof-backed withdrawals and unilateral exits.
  • Addresses — deployed contract addresses (once live).
  • Trust model — what you must trust vs. verify.
  • Architecture — how the engine, sequencer, and contracts fit together.