Skip to content

feat(maturity): RpcSafe / RpcFinalized strategies that follow the source node's block tags - #1330

Open
DylanVerstraete wants to merge 3 commits into
usc-devfrom
feat/rpc-tag-maturity
Open

DylanVerstraete wants to merge 3 commits into
usc-devfrom
feat/rpc-tag-maturity

Conversation

@DylanVerstraete

@DylanVerstraete DylanVerstraete commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Stack (bottom → top; each PR's diff is only its own commits, review in this order)

PR Phase What Base
#1330 Phase 0 RpcSafe / RpcFinalized maturity strategies usc-dev
   #1362 Phase 1a receipts must carry the fetched block's hash #1330
   #1365 Phase 2b prover confirms against the attestation cache #1362
   #1366 Phase 2a archiver follows the latest attested height #1365
   #1367 Phase 1c block tags resolved by cross-provider agreement #1366

Plan: https://claude.ai/code/artifact/400f8668-7b43-4b11-86c8-b8922d8c85ee · Live run against Sepolia safe via Chainstack (2026-09-15): attested heights tracked safe within one interval, archiver ≤ attested ≤ safe at every sample, prover 200 at attested / 422 one above; survived a 20-min RPC outage without restarts.

Why

Every maturity strategy today is a fixed block offset. EvmSafe / EvmFinalized are the Ethereum-epoch approximations (32 / 64 blocks), which on a 2 s rollup such as Base mean 64 s / 128 s and say nothing about whether the block was posted to L1. A fixed count also cannot follow a stalled batcher, and the OP docs note safe can trail for hours in that case.

What

Two new on-chain strategy strings, RpcSafe and RpcFinalized, resolved off-chain to the source node's safe / finalized block tags. Chain-agnostic on purpose: Ethereum, OP-Stack and Arbitrum nodes all serve these tags with their own correct meaning. EvmSafe / EvmFinalized / FixedDelay keep their exact behaviour, so nothing changes for registered chains.

  • primitives / pallet: MaturityStrategy::{RpcSafe, RpcFinalized}, RpcBlockTag, rpc_tag(); is_valid_maturity_strategy accepts the strings. This changes the runtime wasm; no spec bump in this PR (release-owned), so check-version is expected red.
  • eth: Maturity { FixedLag(u64), Tag(BlockTag) } with mature_height, and Client::get_block_number_by_tag using the same fallback-provider walk as block fetches.
  • streams: tip and roots take maturity instead of finalization_lag and resolve it per head. The roots stream now fetches the whole next_unfetched..=mature range, so a safe head that jumps by a full batch is handled; a failed tag lookup is logged and retried on the next head, never guessed.
  • attestor / archiver: resolve the on-chain strategy with stream_eth::maturity_from_strategy.
  • continuity / proof-gen: ContinuityConfig::confirmation_tag and EthRpcProvider::get_block_number_by_tag make the prover confirm blocks on exactly the boundary the attestors attest on. BlockNotOnSourceChain now reports the effective window (tip - confirmed) instead of the configured depth, which is 0 under a tag policy.

Verification

  • New tests: common/eth/tests/block_tag.rs (tag → number, null → not found, clamp to observed head, fallback provider), primitives parse/accessor tests, pallet accept/reject cases, resolver test in stream_eth. All suites of the touched crates pass; clippy -D warnings, fmt, taplo and cargo-machete clean; cargo check -p creditcoin3-runtime passes.

  • Live on Base Sepolia (Chainstack), 3 attestors, local dev node, chain flipped to RpcSafe:

    time (UTC) attested node safe attested − safe
    07:06:30 46584480 46584483 −3
    07:06:50 46584620 46584627 −7
    07:11:15 46584760 46584772 −12
    07:11:20 46584770 46584772 −2

    Attested heights never exceeded safe; they advance in bursts as batches post to L1 (~4–5 min on Base Sepolia). The proof server served a block 72 behind safe (61 continuity roots) and returned BlockNotOnSourceChain for blocks above it.

Notes for operators

  • Registering a chain with RpcSafe / RpcFinalized needs the runtime upgrade first (the pallet validator rejects unknown strings). For the live test above the storage value was written directly on a dev node.
  • Tag-based maturity costs one eth_getBlockByNumber(tag, false) per source head per component.
  • Independent of feat(eth): attest to OP-Stack chains (Base) with a chain-family aware block pipeline #1319 (Base / OP-Stack); the live test ran with this patch applied on top of that branch.

Since approval: rebased onto usc-dev (picked up #1320, #1326, #1335, #1343); no code changes.

@cursor

cursor Bot commented Sep 9, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes how mature heights and proof reorg boundaries are computed across attestors, archiver, and proof-gen; tag-based maturity depends on RPC nodes serving safe/finalized correctly, and misconfiguration can desync archiving from attestation.

Overview
Adds RpcSafe and RpcFinalized on-chain maturity strategies so attestors, the archiver, and the proof server can follow the source node’s safe / finalized tags instead of only fixed block offsets.

Runtime & primitives: MaturityStrategy gains RPC-tag variants plus rpc_tag(); the supported-chains pallet accepts the new strings. Existing EvmSafe / FixedDelay behavior is unchanged.

Off-chain maturity model: eth::Maturity (FixedLag vs Tag) replaces finalization_lag in stream_eth tip/roots. Each new head resolves maturity via mature_height (including Client::get_block_number_by_tag with provider fallback). The roots stream backfills next_unfetched..=mature so tag jumps are handled; failed tag lookups are retried on the next head.

Attestor & archiver: Resolve on-chain strategy with stream_eth::maturity_from_strategy and pass with_maturity(...). Archiver resolve_maturity keeps FINALIZATION_LAG as an override but warns when it disagrees with a tag-based chain.

Continuity / proof-gen: ContinuityConfig::confirmation_tag aligns prover reorg checks with attestors; get_confirmed_last_block uses the tagged height when set. Startup refuses a pinned block_confirmation_depth on tag-following chains. BlockNotOnSourceChain reports the effective window (tip - confirmed).

RPC errors: Unsupported block tags are treated as permanent (no reconnect retry), matching FailedToGetBlockByTag classification.

Reviewed by Cursor Bugbot for commit e666cff. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 91b862b. Configure here.

Comment thread common/continuity/src/builder/mod.rs
@DylanVerstraete
DylanVerstraete requested review from a team, BradleyOlson64, beqaabu, creditcoinprotoclaw, didac-gluwa, jakerumbles and mdbig1 and removed request for a team September 10, 2026 07:25
Comment thread proof-gen-api-server/src/lib.rs Outdated
Comment thread common/continuity/src/rpc.rs
DylanVerstraete added a commit that referenced this pull request Sep 10, 2026
…econnect on an unserved tag

Review follow-ups (#1330):

- proof-gen: `block_confirmation_depth` pinned in config while the on-chain
  MaturityStrategy is RpcSafe/RpcFinalized now fails startup instead of
  warning. A fixed depth cannot reproduce a tag schedule, so the prover would
  confirm blocks the attestors have not attested. Depth-vs-depth
  disagreements keep the existing WARN.

- continuity: `get_block_number_by_tag` preserves the typed
  `FailedToGetBlockByTag` cause (was stringified) and `run()` returns it
  without the reconnect-and-retry loop: a node answering `null` for a tag is
  a permanent property of that node, and reconnecting only churned the shared
  client for every other in-flight request. Classifier
  `eth::anyhow_chain_is_unsupported_block_tag` with tests.

@beqaabu beqaabu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolve conflicts pls

DylanVerstraete added a commit that referenced this pull request Sep 10, 2026
…econnect on an unserved tag

Review follow-ups (#1330):

- proof-gen: `block_confirmation_depth` pinned in config while the on-chain
  MaturityStrategy is RpcSafe/RpcFinalized now fails startup instead of
  warning. A fixed depth cannot reproduce a tag schedule, so the prover would
  confirm blocks the attestors have not attested. Depth-vs-depth
  disagreements keep the existing WARN.

- continuity: `get_block_number_by_tag` preserves the typed
  `FailedToGetBlockByTag` cause (was stringified) and `run()` returns it
  without the reconnect-and-retry loop: a node answering `null` for a tag is
  a permanent property of that node, and reconnecting only churned the shared
  client for every other in-flight request. Classifier
  `eth::anyhow_chain_is_unsupported_block_tag` with tests.
@DylanVerstraete

Copy link
Copy Markdown
Contributor Author

Rebased onto usc-dev; see above.

@gluwa-bot

gluwa-bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Overview

Image reference gluwa/creditcoin3:latest gluwa/creditcoin3:latest
- digest dfb918c3c546 4a870840b291
- tag latest latest
- provenance 7e92a94 cbebdc5
- vulnerabilities critical: 3 high: 16 medium: 21 low: 4 unspecified: 13 critical: 2 high: 13 medium: 11 low: 3 unspecified: 11
- platform linux/amd64 linux/amd64
- size 430 MB 431 MB (+587 kB)
- packages 405 404 (-1)
Base Image ubuntu:26.04
also known as:
latest
resolute
ubuntu:26.04
also known as:
latest
resolute
rolling
- vulnerabilities critical: 1 high: 7 medium: 12 low: 1 unspecified: 2 critical: 1 high: 5 medium: 2 low: 0
Labels (1 changes)
  • ± 1 changed
  • 3 unchanged
-org.opencontainers.image.created=2026-06-27T04:19:04.617438+00:00
+org.opencontainers.image.created=2026-08-17T09:02:45.677319+00:00
 org.opencontainers.image.description=The Ubuntu container image maintained by Canonical

Ubuntu is a Debian-based Linux operating system that runs from the desktop to the cloud, to all your internet connected things.
It is the world's most popular operating system across public clouds and OpenStack clouds.
It is the number one platform for containers; from Docker to Kubernetes to LXD, Ubuntu can run your containers at scale.
Fast, secure and simple, Ubuntu powers millions of PCs worldwide.

 org.opencontainers.image.title=ubuntu
 org.opencontainers.image.version=26.04
Policies (2 improved, 0 worsened)
Policy Name gluwa/creditcoin3:latest gluwa/creditcoin3:latest Change Standing
Default non-root user No Change
No copyleft licenses ⚠️ 373 ⚠️ 361 -12 Improved
No fixable critical or high vulnerabilities ⚠️ 19 ⚠️ 15 -4 Improved
No high-profile vulnerabilities No Change
No outdated base images ⚠️ ⚠️ No Change
No unapproved base images No Change
Supply chain attestations No Change
Packages and Vulnerabilities (47 package changes and 15 vulnerability changes)
  • ➖ 1 packages removed
  • ♾️ 46 packages changed
  • 337 packages unchanged
  • ✔️ 15 vulnerabilities removed
Changes for packages of type deb (35 changes)
Package Version
gluwa/creditcoin3:latest
Version
gluwa/creditcoin3:latest
♾️ base-files 14ubuntu6.1 14ubuntu6.2
♾️ bsdutils 1:2.41.3-3ubuntu2 1:2.41.3-3ubuntu2.2
♾️ curl 8.18.0-1ubuntu2.3 8.18.0-1ubuntu2.5
♾️ diffutils 1:3.12-1 1:3.12-1ubuntu0.1
♾️ gnu-coreutils 9.7-3ubuntu2 9.7-3ubuntu2.1
♾️ gpgv 2.4.8-4ubuntu3 2.4.8-4ubuntu3.1
♾️ libattr1 1:2.5.2-4 1:2.5.2-4ubuntu0.1
♾️ libaudit-common 1:4.1.2-1build1 1:4.1.2-1ubuntu0.1
♾️ libaudit1 1:4.1.2-1build1 1:4.1.2-1ubuntu0.1
♾️ libblkid1 2.41.3-3ubuntu2 2.41.3-3ubuntu2.2
♾️ libbz2-1.0 1.0.8-6build2 1.0.8-6ubuntu0.1
♾️ libc-bin 2.43-2ubuntu2.3 2.43-2ubuntu2.4
♾️ libc-gconv-modules-extra 2.43-2ubuntu2.3 2.43-2ubuntu2.4
♾️ libc6 2.43-2ubuntu2.3 2.43-2ubuntu2.4
♾️ libcurl4t64 8.18.0-1ubuntu2.3 8.18.0-1ubuntu2.5
♾️ libgcrypt20 1.12.0-2ubuntu1 1.12.0-2ubuntu1.1
♾️ libmount1 2.41.3-3ubuntu2 2.41.3-3ubuntu2.2
♾️ libpam-modules 1.7.0-5ubuntu3.1 1.7.0-5ubuntu3.2
♾️ libpam-modules-bin 1.7.0-5ubuntu3.1 1.7.0-5ubuntu3.2
♾️ libpam-runtime 1.7.0-5ubuntu3.1 1.7.0-5ubuntu3.2
♾️ libpam0g 1.7.0-5ubuntu3.1 1.7.0-5ubuntu3.2
♾️ libpq5 18.4-0ubuntu0.26.04.1 18.6-0ubuntu0.26.04.1
♾️ libsmartcols1 2.41.3-3ubuntu2 2.41.3-3ubuntu2.2
♾️ libssh2-1t64 1.11.1-1ubuntu0.26.04.3 1.11.1-1ubuntu0.26.04.4
♾️ libssl3t64 3.5.5-1ubuntu3.3 3.5.5-1ubuntu3.5
♾️ libsystemd0 259.5-0ubuntu3.3 259.5-0ubuntu3.4
♾️ libudev1 259.5-0ubuntu3.3 259.5-0ubuntu3.4
♾️ libuuid1 2.41.3-3ubuntu2 2.41.3-3ubuntu2.2
♾️ login 1:4.16.0-2+really2.41.3-3ubuntu2 1:4.16.0-2+really2.41.3-3ubuntu2.2
♾️ mount 2.41.3-3ubuntu2 2.41.3-3ubuntu2.2
♾️ openssl 3.5.5-1ubuntu3.3 3.5.5-1ubuntu3.5
♾️ openssl-provider-legacy 3.5.5-1ubuntu3.3 3.5.5-1ubuntu3.5
♾️ perl-base 5.40.1-7ubuntu0.1 5.40.1-7ubuntu0.3
♾️ util-linux 2.41.3-3ubuntu2 2.41.3-3ubuntu2.2
♾️ zlib1g 1:1.3.dfsg+really1.3.1-1ubuntu3 1:1.3.dfsg+really1.3.1-1ubuntu3.1
Changes for packages of type golang (6 changes)
Package Version
gluwa/creditcoin3:latest
Version
gluwa/creditcoin3:latest
♾️ github.com/canonical/pebble 1.31.1-0.20260528050051-33f10658d3fd 1.32.2-0.20260721212935-faa1696b477d
♾️ github.com/gorilla/websocket 1.5.1 1.5.3
critical: 0 high: 0 medium: 1 low: 0
Removed vulnerabilities (1):
  • medium : GHSA--w67g--5rqw--f597
golang.org/x/net 0.40.0
critical: 1 high: 2 medium: 7 low: 0
Removed vulnerabilities (10):
  • critical : CVE--2026--39821
  • high : CVE--2026--46600
  • high : CVE--2026--33814
  • medium : CVE--2026--25680
  • medium : CVE--2026--42506
  • medium : CVE--2026--42502
  • medium : CVE--2026--27136
  • medium : CVE--2026--25681
  • medium : CVE--2025--58190
  • medium : CVE--2025--47911
♾️ golang.org/x/sys 0.33.0 0.46.0
critical: 0 high: 0 medium: 0 low: 1
Removed vulnerabilities (1):
  • low : CVE--2026--39824
♾️ golang.org/x/term 0.32.0 0.44.0
♾️ stdlib 1.26.3 1.26.5
critical: 1 high: 6 medium: 4 low: 0 unspecified: 2 critical: 1 high: 5 medium: 2 low: 0
Removed vulnerabilities (5):
  • high : CVE--2026--42504
  • medium : CVE--2026--27145
  • medium : CVE--2026--42507
  • unspecified : CVE--2026--42505
  • unspecified : CVE--2026--39822
Changes for packages of type npm (6 changes)
Package Version
gluwa/creditcoin3:latest
Version
gluwa/creditcoin3:latest
♾️ @types/node 26.1.2 22.7.5
♾️ node-gyp 13.0.1 13.0.2
♾️ picomatch 4.0.5 4.0.7
♾️ undici 8.10.0 8.10.2
♾️ undici-types 8.3.0 6.21.0
♾️ ws 8.21.2 8.21.3

… source node's block tags

The existing maturity strategies are all fixed block offsets: `EvmSafe` and
`EvmFinalized` are the Ethereum-epoch approximations (32 / 64 blocks) and mean
64 s / 128 s on a 2 s rollup, where they say nothing about L1 posting. A fixed
count also cannot follow a stalled batcher.

This adds two on-chain strategy strings, `RpcSafe` and `RpcFinalized`, that the
off-chain components resolve through `eth_getBlockByNumber("safe" | "finalized")`
on the source node. On Ethereum that tracks justification / finality; on OP-Stack
and Arbitrum rollups it tracks L1 batch inclusion / L1 finality. The existing
strategies keep their exact behaviour, so no live chain changes schedule.

- primitives: `MaturityStrategy::{RpcSafe, RpcFinalized}`, `RpcBlockTag`,
  `rpc_tag()`; pallet validator accepts the new strings (runtime wasm changes,
  no spec bump here).
- eth: `Maturity { FixedLag, Tag }` with `mature_height`, and
  `Client::get_block_number_by_tag` walking fallback providers like block fetches.
- streams: tip and roots streams take `maturity` instead of `finalization_lag`
  and resolve it per head; the roots stream fetches the whole newly mature range
  so a `safe` head that jumps by a batch is handled.
- attestor / archiver: resolve the on-chain strategy via
  `stream_eth::maturity_from_strategy`.
- continuity / proof-gen: `confirmation_tag` makes the prover confirm blocks on
  the same boundary the attestors attest on; the not-confirmed error reports the
  effective window.

Verified with mock-RPC tests and live on Base Sepolia (Chainstack): attested
heights stayed 2-12 blocks below the node's `safe` head and never above it,
advancing in bursts as batches posted to L1.
…d provider

`ArchiverEthProvider` forwards every live-node call to its ETH fallback but
inherited the erroring default for `get_block_number_by_tag`, so a proof server
configured with an `archiver_url` on an `RpcSafe` / `RpcFinalized` chain would
fail `get_confirmed_last_block`. Forward it like `get_last_block`, teach the mock
provider fixed `safe` / `finalized` heights, and cover both the tag-based
confirmed tip and the archiver forwarding with tests.
…econnect on an unserved tag

Review follow-ups (#1330):

- proof-gen: `block_confirmation_depth` pinned in config while the on-chain
  MaturityStrategy is RpcSafe/RpcFinalized now fails startup instead of
  warning. A fixed depth cannot reproduce a tag schedule, so the prover would
  confirm blocks the attestors have not attested. Depth-vs-depth
  disagreements keep the existing WARN.

- continuity: `get_block_number_by_tag` preserves the typed
  `FailedToGetBlockByTag` cause (was stringified) and `run()` returns it
  without the reconnect-and-retry loop: a node answering `null` for a tag is
  a permanent property of that node, and reconnecting only churned the shared
  client for every other in-flight request. Classifier
  `eth::anyhow_chain_is_unsupported_block_tag` with tests.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants