Skip to content

fix(eth): reject receipts that belong to a different block than the one fetched - #1362

Open
DylanVerstraete wants to merge 1 commit into
feat/rpc-tag-maturityfrom
feat/hash-pinned-source-reads
Open

DylanVerstraete wants to merge 1 commit into
feat/rpc-tag-maturityfrom
feat/hash-pinned-source-reads

Conversation

@DylanVerstraete

@DylanVerstraete DylanVerstraete commented Sep 15, 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.

First step of the plan to make the attested set the single maturity boundary: https://claude.ai/code/artifact/400f8668-7b43-4b11-86c8-b8922d8c85ee

The problem

get_block and get_block_receipts are two separate height-keyed calls, issued concurrently to one provider URL. That URL is commonly a load balancer over many nodes, so the pair can be answered by peers on opposite sides of a reorg.

Today that is caught indirectly and only sometimes. try_from_fetched_block recomputes the transaction and receipt roots and compares them with the header, which:

  • detects the mismatch only for blocks that have transactions, since the empty-block carve-out returns before the check;
  • reports it as BlockHeaderRootsMismatch, "a root that failed to reproduce", rather than naming the actual fault;
  • costs a full provider sweep and up to five retry rounds in try_fetch_block to work around something that is directly observable.

The change

Receipts already carry the hash of the block they belong to. Comparing it with the fetched block's hash is exact and costs no extra round trip. New Error::ReceiptsBlockMismatch { number, block, receipts }, classified alongside the other payload inconsistencies so it walks to the next provider rather than triggering a reconnect, exactly as the root mismatch does today.

Receipts whose block_hash is absent are skipped rather than rejected. The field is optional in the RPC schema and some providers omit it; those payloads still face the root check as before.

Why verify by hash rather than fetch by hash

The original instruction was to fetch receipts by hash. I measured the cost and went with verification instead, which reaches the same guarantee for free.

Fetching receipts by hash requires the block first, so the two calls stop being try_join!-concurrent and per-block latency roughly doubles. Throughput is bounded by concurrency divided by latency, so at the current limits of 20 for continuity and 8 for the archiver backfill this would roughly halve block-fetch throughput. The archiver sweep was measured at 103 blocks/sec, and the BSC historical plan sizes its multi-day run on that number.

Verification catches the same fault at the same point, with the same error, at zero latency cost. Happy to switch to true by-hash fetching if you would rather have the stronger construction and pay for it with concurrency, but it did not look like a good trade.

Tests

common/eth/tests/receipt_block_match.rs, four cases: mismatched hashes are rejected and classify for the fallback walk with the right block-number hint; matching hashes get past the check; an absent block_hash is treated as unverifiable rather than a mismatch; the empty-block carve-out still returns Ok, since it is load-bearing for Substrate and Frontier dev chains.

cargo test -p eth -p archiver -p continuity, clippy -D warnings across eth, archiver, continuity, attestor and proof-gen, fmt clean.

Note for anyone running these locally: cargo test -p eth alone fails to unify tokio's signal feature and attestor-primitives/std. Test it with at least one dependent crate in the same invocation.

@DylanVerstraete
DylanVerstraete requested review from a team, BradleyOlson64, beqaabu, creditcoinprotoclaw, didac-gluwa, jakerumbles and mdbig1 and removed request for a team September 15, 2026 10:08
@cursor

cursor Bot commented Sep 15, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes block assembly validation on the critical eth RPC path; behavior is narrowly scoped (hash compare + same fallback classification as existing mismatch errors) with dedicated tests.

Overview
Adds an early, explicit guard when assembling blocks from separate get_block and get_block_receipts RPC responses: if any receipt includes a blockHash that differs from the fetched block header, assembly fails with Error::ReceiptsBlockMismatch instead of only surfacing an indirect header-root mismatch.

That variant is treated like other payload-inconsistency errors (fallback to the next RPC endpoint, block-number hint for callers). Receipts with no blockHash are unchanged—they still rely on the existing root checks. Empty blocks with no receipts are unaffected.

New integration tests in receipt_block_match.rs cover mismatch rejection, matching hashes, omitted blockHash, and the empty-block path.

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

@DylanVerstraete
DylanVerstraete added this pull request to stack #1368 September 15, 2026 13:40
DylanVerstraete added a commit that referenced this pull request Sep 15, 2026
…he block hash

Phase 1c, and the second half of 1a, of making the attestor's maturity
decision worth following.

get_block_number_by_tag walked the provider list and returned the first
block any provider served, and it received the full block and returned only
its number. For a safety boundary that is the wrong shape twice over: a
single load-balanced peer on a stale fork could set the boundary for the
whole attestor, and the one datum that pins block identity was thrown away
at the one place it was free.

New get_block_by_tag returns TaggedBlock { number, hash } and asks every
configured provider at once. The lowest reported height is the candidate,
because a provider further along has by definition already passed it. Every
other responder must then have the candidate block under the same hash:
same-height responders already said so, further-along responders are asked
for the block at that height. A different hash is
Error::BlockTagDisagreement, and the lookup refuses to pick a side; the tip
and roots streams already log and retry on the next head. A provider that
reports the tag past a height it cannot serve is broken, not forked, and
fails the lookup outright. One that errors on the confirmation read has
already voted the candidate mature and is warned about, as tag-call errors
were before.

Providers that error or answer null still do not take part, so with one
provider this degenerates to that provider's answer: per-replica provider
diversity in the deployment is what gives the check its teeth, and this is
the code side of that. get_block_number_by_tag stays as a thin wrapper.

The hash stops at the boundary decision on purpose. Fetching the blocks
themselves by hash would serialise get_block and get_block_receipts and
roughly halve archiver throughput; #1362's receipts-to-block hash check
already pins each fetched block's integrity at no cost.

Also removed: EthRpcProvider::get_block_number_by_tag in continuity, dead
since #1365 took the prover's maturity opinion away.

Tests: agreement takes the lowest height and confirms it against the further
provider (both are asked every time); a forked provider is a disagreement at
different heights and at the same height; the disagreement surfaces through
Maturity as a per-head error. The mock chain now salts block hashes so two
mocks can be on one chain or on different forks.
…ne fetched

First step of making the attestor the maturity authority: before anything
starts following its boundary, its own source reads have to be internally
consistent.

get_block and get_block_receipts are two separate height-keyed calls, and a
provider URL is commonly a load balancer over many nodes, so the pair can be
answered by peers on opposite sides of a reorg. Today that is inferred
indirectly, and only sometimes: try_from_fetched_block recomputes the
transaction and receipt roots and compares them with the header, which
detects the mismatch for blocks that have transactions but skips the check
entirely for empty blocks, and reports it as BlockHeaderRootsMismatch, a
root that failed to reproduce rather than the actual fault.

Receipts already carry the hash of the block they belong to. Comparing it
with the fetched block's hash is exact, costs no extra round trip, and names
the real problem. New Error::ReceiptsBlockMismatch is classified with the
other payload inconsistencies, so it walks to the next provider rather than
triggering a reconnect, matching how the root mismatch is handled today.

Receipts whose block_hash is absent are skipped rather than rejected: the
field is optional in the RPC schema and some providers omit it, and those
payloads still face the root check exactly as before.

Tests: mismatched hashes are rejected and classify for the fallback walk;
matching hashes get past the check; an absent block_hash is treated as
unverifiable rather than a mismatch; the empty-block carve-out is unchanged.
DylanVerstraete added a commit that referenced this pull request Sep 16, 2026
…he block hash

Phase 1c, and the second half of 1a, of making the attestor's maturity
decision worth following.

get_block_number_by_tag walked the provider list and returned the first
block any provider served, and it received the full block and returned only
its number. For a safety boundary that is the wrong shape twice over: a
single load-balanced peer on a stale fork could set the boundary for the
whole attestor, and the one datum that pins block identity was thrown away
at the one place it was free.

New get_block_by_tag returns TaggedBlock { number, hash } and asks every
configured provider at once. The lowest reported height is the candidate,
because a provider further along has by definition already passed it. Every
other responder must then have the candidate block under the same hash:
same-height responders already said so, further-along responders are asked
for the block at that height. A different hash is
Error::BlockTagDisagreement, and the lookup refuses to pick a side; the tip
and roots streams already log and retry on the next head. A provider that
reports the tag past a height it cannot serve is broken, not forked, and
fails the lookup outright. One that errors on the confirmation read has
already voted the candidate mature and is warned about, as tag-call errors
were before.

Providers that error or answer null still do not take part, so with one
provider this degenerates to that provider's answer: per-replica provider
diversity in the deployment is what gives the check its teeth, and this is
the code side of that. get_block_number_by_tag stays as a thin wrapper.

The hash stops at the boundary decision on purpose. Fetching the blocks
themselves by hash would serialise get_block and get_block_receipts and
roughly halve archiver throughput; #1362's receipts-to-block hash check
already pins each fetched block's integrity at no cost.

Also removed: EthRpcProvider::get_block_number_by_tag in continuity, dead
since #1365 took the prover's maturity opinion away.

Tests: agreement takes the lowest height and confirms it against the further
provider (both are asked every time); a forked provider is a disagreement at
different heights and at the same height; the disagreement surfaces through
Maturity as a per-head error. The mock chain now salts block hashes so two
mocks can be on one chain or on different forks.
@DylanVerstraete
DylanVerstraete force-pushed the feat/hash-pinned-source-reads branch from 77d81fa to 21cb372 Compare September 16, 2026 08:57
DylanVerstraete added a commit that referenced this pull request Sep 16, 2026
…he block hash

Phase 1c, and the second half of 1a, of making the attestor's maturity
decision worth following.

get_block_number_by_tag walked the provider list and returned the first
block any provider served, and it received the full block and returned only
its number. For a safety boundary that is the wrong shape twice over: a
single load-balanced peer on a stale fork could set the boundary for the
whole attestor, and the one datum that pins block identity was thrown away
at the one place it was free.

New get_block_by_tag returns TaggedBlock { number, hash } and asks every
configured provider at once. The lowest reported height is the candidate,
because a provider further along has by definition already passed it. Every
other responder must then have the candidate block under the same hash:
same-height responders already said so, further-along responders are asked
for the block at that height. A different hash is
Error::BlockTagDisagreement, and the lookup refuses to pick a side; the tip
and roots streams already log and retry on the next head. A provider that
reports the tag past a height it cannot serve is broken, not forked, and
fails the lookup outright. One that errors on the confirmation read has
already voted the candidate mature and is warned about, as tag-call errors
were before.

Providers that error or answer null still do not take part, so with one
provider this degenerates to that provider's answer: per-replica provider
diversity in the deployment is what gives the check its teeth, and this is
the code side of that. get_block_number_by_tag stays as a thin wrapper.

The hash stops at the boundary decision on purpose. Fetching the blocks
themselves by hash would serialise get_block and get_block_receipts and
roughly halve archiver throughput; #1362's receipts-to-block hash check
already pins each fetched block's integrity at no cost.

Also removed: EthRpcProvider::get_block_number_by_tag in continuity, dead
since #1365 took the prover's maturity opinion away.

Tests: agreement takes the lowest height and confirms it against the further
provider (both are asked every time); a forked provider is a disagreement at
different heights and at the same height; the disagreement surfaces through
Maturity as a per-head error. The mock chain now salts block hashes so two
mocks can be on one chain or on different forks.
@gluwa-bot

Copy link
Copy Markdown
Contributor

Overview

Image reference gluwa/creditcoin3:latest gluwa/creditcoin3:latest
- digest dfb918c3c546 348afa5be9fc
- tag latest latest
- provenance 7e92a94 844a130
- 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 (+624 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

DylanVerstraete added a commit that referenced this pull request Sep 16, 2026
…he block hash

Phase 1c, and the second half of 1a, of making the attestor's maturity
decision worth following.

get_block_number_by_tag walked the provider list and returned the first
block any provider served, and it received the full block and returned only
its number. For a safety boundary that is the wrong shape twice over: a
single load-balanced peer on a stale fork could set the boundary for the
whole attestor, and the one datum that pins block identity was thrown away
at the one place it was free.

New get_block_by_tag returns TaggedBlock { number, hash } and asks every
configured provider at once. The lowest reported height is the candidate,
because a provider further along has by definition already passed it. Every
other responder must then have the candidate block under the same hash:
same-height responders already said so, further-along responders are asked
for the block at that height. A different hash is
Error::BlockTagDisagreement, and the lookup refuses to pick a side; the tip
and roots streams already log and retry on the next head. A provider that
reports the tag past a height it cannot serve is broken, not forked, and
fails the lookup outright. One that errors on the confirmation read has
already voted the candidate mature and is warned about, as tag-call errors
were before.

Providers that error or answer null still do not take part, so with one
provider this degenerates to that provider's answer: per-replica provider
diversity in the deployment is what gives the check its teeth, and this is
the code side of that. get_block_number_by_tag stays as a thin wrapper.

The hash stops at the boundary decision on purpose. Fetching the blocks
themselves by hash would serialise get_block and get_block_receipts and
roughly halve archiver throughput; #1362's receipts-to-block hash check
already pins each fetched block's integrity at no cost.

Also removed: EthRpcProvider::get_block_number_by_tag in continuity, dead
since #1365 took the prover's maturity opinion away.

Tests: agreement takes the lowest height and confirms it against the further
provider (both are asked every time); a forked provider is a disagreement at
different heights and at the same height; the disagreement surfaces through
Maturity as a per-head error. The mock chain now salts block hashes so two
mocks can be on one chain or on different forks.
DylanVerstraete added a commit that referenced this pull request Sep 16, 2026
…he block hash

Phase 1c, and the second half of 1a, of making the attestor's maturity
decision worth following.

get_block_number_by_tag walked the provider list and returned the first
block any provider served, and it received the full block and returned only
its number. For a safety boundary that is the wrong shape twice over: a
single load-balanced peer on a stale fork could set the boundary for the
whole attestor, and the one datum that pins block identity was thrown away
at the one place it was free.

New get_block_by_tag returns TaggedBlock { number, hash } and asks every
configured provider at once. The lowest reported height is the candidate,
because a provider further along has by definition already passed it. Every
other responder must then have the candidate block under the same hash:
same-height responders already said so, further-along responders are asked
for the block at that height. A different hash is
Error::BlockTagDisagreement, and the lookup refuses to pick a side; the tip
and roots streams already log and retry on the next head. A provider that
reports the tag past a height it cannot serve is broken, not forked, and
fails the lookup outright. One that errors on the confirmation read has
already voted the candidate mature and is warned about, as tag-call errors
were before.

Providers that error or answer null still do not take part, so with one
provider this degenerates to that provider's answer: per-replica provider
diversity in the deployment is what gives the check its teeth, and this is
the code side of that. get_block_number_by_tag stays as a thin wrapper.

The hash stops at the boundary decision on purpose. Fetching the blocks
themselves by hash would serialise get_block and get_block_receipts and
roughly halve archiver throughput; #1362's receipts-to-block hash check
already pins each fetched block's integrity at no cost.

Also removed: EthRpcProvider::get_block_number_by_tag in continuity, dead
since #1365 took the prover's maturity opinion away.

Tests: agreement takes the lowest height and confirms it against the further
provider (both are asked every time); a forked provider is a disagreement at
different heights and at the same height; the disagreement surfaces through
Maturity as a per-head error. The mock chain now salts block hashes so two
mocks can be on one chain or on different forks.
DylanVerstraete added a commit that referenced this pull request Sep 16, 2026
…he block hash

Phase 1c, and the second half of 1a, of making the attestor's maturity
decision worth following.

get_block_number_by_tag walked the provider list and returned the first
block any provider served, and it received the full block and returned only
its number. For a safety boundary that is the wrong shape twice over: a
single load-balanced peer on a stale fork could set the boundary for the
whole attestor, and the one datum that pins block identity was thrown away
at the one place it was free.

New get_block_by_tag returns TaggedBlock { number, hash } and asks every
configured provider at once. The lowest reported height is the candidate,
because a provider further along has by definition already passed it. Every
other responder must then have the candidate block under the same hash:
same-height responders already said so, further-along responders are asked
for the block at that height. A different hash is
Error::BlockTagDisagreement, and the lookup refuses to pick a side; the tip
and roots streams already log and retry on the next head. A provider that
reports the tag past a height it cannot serve is broken, not forked, and
fails the lookup outright. One that errors on the confirmation read has
already voted the candidate mature and is warned about, as tag-call errors
were before.

Providers that error or answer null still do not take part, so with one
provider this degenerates to that provider's answer: per-replica provider
diversity in the deployment is what gives the check its teeth, and this is
the code side of that. get_block_number_by_tag stays as a thin wrapper.

The hash stops at the boundary decision on purpose. Fetching the blocks
themselves by hash would serialise get_block and get_block_receipts and
roughly halve archiver throughput; #1362's receipts-to-block hash check
already pins each fetched block's integrity at no cost.

Also removed: EthRpcProvider::get_block_number_by_tag in continuity, dead
since #1365 took the prover's maturity opinion away.

Tests: agreement takes the lowest height and confirms it against the further
provider (both are asked every time); a forked provider is a disagreement at
different heights and at the same height; the disagreement surfaces through
Maturity as a per-head error. The mock chain now salts block hashes so two
mocks can be on one chain or on different forks.
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.

4 participants