Skip to content

Modes, SLA & Finality

This page covers how anchoring is governed: which mode a point is anchored in, how that policy is resolved, how a proof moves to finality, and what happens when the chain reorganizes, or the backlog grows.

CORE-M supports three anchoring modes. The mode determines how a point’s hash reaches the chain.

Merkle batch (default). Point hashes accumulate into a batch, a binary SHA-256 Merkle tree is built, and only the 32-byte root is anchored in one transaction. Most cost-efficient — one transaction commits up to a full batch of points. Verification needs the per-point Merkle path. Latency is bounded by the flush window. See Hashing & Merkle Trees.

Anchoring policy is enforced at three levels. The effective policy is resolved most-specific-first — the first level that defines a field wins:

device override -> device profile -> tenant default -> platform default
(most specific) (least specific)

So a device override beats its device profile, which beats the tenant default, which beats the built-in platform default. For example, if a tenant defaults to merkle_batch but the critical-sensors device profile sets per_event with required=true, a device using that profile is anchored per-event. Device profiles are described in Device Profiles.

FieldMeaning
modemerkle_batch, per_event, or hash_chain.
requiredWhether anchoring is mandatory for the point to be accepted.
max_pending_age_secondsMaximum age a point may sit pending before escalation.
finality_confirmationsConfirmations required before a proof is final (default 6).
backlog_actionaccept_pending, throttle, or reject when the backlog is high.
max_backlog_pointsBacklog threshold that triggers the backlog action.

Every accepted point has an explicit lifecycle record (in Aerospike, namespace telemetry, set proof_state, keyed by {tenant_id}:{device_id}:{data_hash_hex}, updated with generation-checked CAS). It moves through these states:

stateDiagram-v2
  [*] --> accepted: hash + lifecycle record written
  accepted --> validated: written to telemetry stores
  validated --> pending_batch: assigned to a batch
  pending_batch --> anchored: anchor tx broadcast (txid known)
  anchored --> confirmed: block inclusion observed
  confirmed --> final: confirmation depth >= threshold
  anchored --> failed_retryable: temporary failure
  confirmed --> failed_retryable: reorg invalidates tx
  failed_retryable --> pending_batch: retry scheduled
  failed_retryable --> failed_terminal: permanent failure
  final --> [*]
  failed_terminal --> [*]
StateMeaning
acceptedPassed auth and parsing; lifecycle record exists. The point is not acknowledged to the caller until this write succeeds.
validatedDevice/profile validation passed; written to telemetry stores.
pending_batchHash assigned to an anchoring batch.
anchoredAnchoring transaction broadcast; txid known.
confirmedARC or the SPV watcher observed block inclusion.
finalConfirmation depth reached the finality threshold.
failed_retryableTemporary failure; a retry is scheduled.
failed_terminalPermanent failure requiring operator action.

A confirmed transaction is not immediately treated as irreversible. The finality watcher tracks confirmations for every anchored txid (finality records live in Aerospike, namespace anchors, set finality, keyed by {txid}). When the confirmation depth reaches the policy’s finality_confirmations (default 6), the finality record is marked final and every proof-lifecycle record for that txid moves confirmed → final, with a anchor.lifecycle.final.<tenant> event published.

Finality survives restarts: on startup the watcher reloads finality state and will not downgrade already-final records or re-emit finality events.

If a chain reorganization removes the block that contained an anchored transaction, the proof must not silently claim finality. The watcher detects, via the SPV/ARC watcher, that the block holding a txid is no longer on the best chain, and then:

  1. Sets the finality state for that txid to reorged.
  2. Moves affected proof-lifecycle records to failed_retryable.
  3. Retains the previous txid, block_height, and block_hash in a reorg_history field.
  4. Republishes the affected hashes to the correct anchoring queue so they are re-anchored.
  5. Increments corem_anchor_reorg_events_total.

When pending work piles up, the policy’s backlog_action decides what happens to new points once max_backlog_points is exceeded:

ActionBehavior
accept_pendingKeep accepting points; they queue as pending.
throttleReject above the allowed throttled rate with retry-after metadata; no lifecycle record is created for rejected points. An audit event anchoring.backlog.throttled is written at most once per configured interval.
rejectReject new points with RESOURCE_EXHAUSTED / HTTP 429 and reason="anchoring_backlog_exceeded".

Both throttle and reject increment their respective metrics (corem_anchor_backlog_rejections_total{tenant} for rejections).

Anchoring spend is attributed back to tenants for cost accounting. Usage is aggregated in Aerospike (namespace anchors, set tenant_usage, keyed by {tenant_id}:{yyyymmdd}). When an anchoring transaction is broadcast, the usage records for every tenant represented in that batch are updated atomically — a batch of 900 points for tenant T1 and 100 for T2 updates both. Each record carries point_count, batch_count, token_count, estimated_fee_sats, and the list of txids, and the update is idempotent on retry using the batch_id as the idempotency key.

Next

Continue to Verification to see how a final proof is checked independently and exported as a portable bundle.