APIConsensus
On‑chain consensus for provider‑signed snapshots. Nodes submit EIP‑712 Snapshots; the contract tallies identical digests and finalizes either when quorum is reached or after expiry + grace. It can optionally integrate with NodeRegistry for eligibility and slashing/rewards.
Units: timestamps in milliseconds (
uint64). Amounts are SYL (18d) only when interacting with Escrow/NodeRegistry. Contract is UUPS‑upgradeable, ownable, pausable, reentrancy‑guarded.
Responsibilities
- Register requests (mirroring AccessRegistry IDs & nonces).
- Verify Provider EIP‑712 signatures over snapshots.
- Enforce freshness (skew/TTL) and one‑vote‑per‑node (optionally gated by NodeRegistry).
- Tally votes with deterministic fork‑choice; detect ProviderEquivocation.
- Finalize success/failure and notify APIEscrow to settle/refund.
- (Optional) Slash mismatching nodes and reward honest ones via NodeRegistry.
Snapshot (what nodes submit)
struct Snapshot {
bytes32 apiId; // logical API identifier
uint256 seqNo; // provider sequence (monotonic if enabled per API)
uint64 providerTs; // provider timestamp (ms)
uint64 ttl; // ms; 0 = no TTL (skew check still applies)
bytes32 contentHash; // hash of the response artifact/root
}EIP‑712 domain: name = "SylanProviderSnapshot", version = "1"
Typehash: keccak256("Snapshot(bytes32 apiId,uint256 seqNo,uint64 providerTs,uint64 ttl,bytes32 contentHash)")
Checks on submitSnapshot
- API active (from AccessRegistry).
providerSignerOf(apiId)!= 0x0 and recovered signer matches.- Freshness:
providerTs ≤ nowMs + maxSkewMs(apiId); ifttl ≠ 0, thennowMs ≤ providerTs + min(ttl, maxTtlMs(apiId)). - Eligibility: if
nodeRegistry != 0, caller must beisActiveNode. - One vote per node per request (local guard).
Equivocation detection: first contentHash for (apiId, seqNo) is remembered; a different hash later triggers ProviderEquivocation.
Fork‑choice & finalization
Vote digest = EIP‑712 hash of the Snapshot.
Leader selection:
- More votes wins.
- If tied, higher
seqNowins. - If still tied, lower
providerTswins.
When does it finalize?
- Early quorum: if leader votes
≥ quorum, finalize immediately. - Deadline path: callable when
nowMs ≥ expiresAtMsandnowMs ≤ expiresAtMs + requestExpiryGraceMs. - Inactive API at finalize time →
RequestFailed(reason=2 /*InactiveAPI*/)and Escrow refunds. - Seq monotonicity (per‑API flag from AccessRegistry): if enabled and the candidate’s
seqNoregresses, treat asNoQuorumfailure.
Views (read model)
accessRegistry() → addressnodeRegistry() → addresseventLogger() → addressapiEscrow() → addressquorum() → uint256requestExpiryGraceMs() → uint64slashAmount() → uint16(bps of stake)requestMeta(requestId) → { apiId, consumer, expiresAtMs, status }(status: 0=Unset,1=Open,2=Finalized,3=Failed)topCandidate(requestId) → (msgHash, votes, seqNo, providerTs, contentHash)
Core flow (writes)
registerRequest(requestId, apiId, consumer, nonce, expiresAtMs)
Called by APIEscrow when a PPC lock occurs. Recomputes the deterministic requestId and checks the nonce against AccessRegistry, then sets status Open and emits RequestRegistered.
submitSnapshot(requestId, snapshot, providerSig, pointerURI)
Called by Nodes (optionally gated by NodeRegistry). Verifies signature & freshness, records a vote (one per node), updates fork‑choice, emits ResponseSubmitted, and may finalize early if quorum.
finalize(requestId)
Anyone can call once the deadline path is available. Will finalize or fail (NoQuorum/InactiveAPI) and notify Escrow accordingly.
Events
RequestRegistered(bytes32 indexed requestId, bytes32 indexed apiId, address indexed consumer, uint64 expiresAtMs, uint256 nonce)ResponseSubmitted(bytes32 indexed requestId, address indexed node, bytes32 msgHash, uint256 seqNo, uint64 providerTs, bytes32 contentHash, string pointerURI)RequestFinalized(bytes32 indexed requestId, bytes32 indexed apiId, uint256 seqNo, uint64 providerTs, bytes32 contentHash, bytes32 msgHash, uint256 votes)RequestFailed(bytes32 indexed requestId, bytes32 indexed apiId, uint8 reason)—1 = NoQuorum,2 = InactiveAPIProviderEquivocation(bytes32 indexed apiId, uint256 indexed seqNo, bytes32 firstHash, bytes32 laterHash)
The contract can optionally forward human‑readable logs to EventLogger (e.g.,
Finalized/Failedwith JSON metadata) for indexers.
Slashing & rewards (optional NodeRegistry)
When configured:
- Slash amount:
slashAmountbps is applied to each mismatching node’s stake upon finalization. - The total slashed is split between nodePool and treasury/burn per NodeRegistry’s BPS policy.
- Honest nodes (those who voted for the winning digest) may be rewarded from the node pool, and reputation is increased.
If
nodeRegistry == 0x0, eligibility and slashing are disabled, but consensus still operates.
Minimal ABI (dapp‑facing)
These are the typical methods UIs and nodes need; full interface lives in the repo.
[
{"type":"function","stateMutability":"view","name":"quorum","inputs":[],"outputs":[{"type":"uint256"}]},
{"type":"function","stateMutability":"view","name":"requestExpiryGraceMs","inputs":[],"outputs":[{"type":"uint64"}]},
{"type":"function","stateMutability":"view","name":"topCandidate","inputs":[{"name":"requestId","type":"bytes32"}],"outputs":[
{"type":"bytes32"},{"type":"uint256"},{"type":"uint256"},{"type":"uint64"},{"type":"bytes32"}
]},
{"type":"function","stateMutability":"nonpayable","name":"submitSnapshot","inputs":[
{"name":"requestId","type":"bytes32"},
{"name":"snapshot","type":"tuple","components":[
{"name":"apiId","type":"bytes32"},
{"name":"seqNo","type":"uint256"},
{"name":"providerTs","type":"uint64"},
{"name":"ttl","type":"uint64"},
{"name":"contentHash","type":"bytes32"}
]},
{"name":"providerSig","type":"bytes"},
{"name":"pointerURI","type":"string"}
],"outputs":[]},
{"type":"function","stateMutability":"nonpayable","name":"finalize","inputs":[{"name":"requestId","type":"bytes32"}],"outputs":[]}
]Integration tips
- Pointer vs integrity:
pointerURIis only a hint; UIs should verifycontentHashwhen fetching artifacts. - Signer rotation windows: if AccessRegistry returns
0x0fromproviderSignerOf(apiId), nodes must treat snapshots as unauthorized until the timelock elapses. - Event‑driven UI: subscribe to
RequestRegistered→ResponseSubmitted*→ (RequestFinalized|RequestFailed). Then readAPIEscrow.withdrawableOfto show payouts/refunds. - Idempotency: Escrow calls from consensus are safe to retry. Don’t fear duplicate finalize attempts; the state machine prevents double settlement.
Admin & safety
- Owner can
setQuorum(q)(q > 0) andsetRequestExpiryGraceMs(ms); pausing disables writes. - Deterministic IDs:
registerRequestrecomputes therequestIdexactly as in AccessRegistry and checks thenoncewindow. - Millis everywhere: clients and nodes should pass/expect ms for all consensus timestamps.
Out of scope for this page
- Pricing/escrow math → APIEscrow
- API plan/params and signer management → AccessRegistry
- Node staking and slashing policy → NodeRegistry
- Addresses & minimal ABIs → Architecture → Addresses & ABIs