Tagged RPC heads at heights three and two follow parent paths to a common ancestor at height two.
Teaching model of evidence admission; destination approval and payout are separate.

Two RPC responses can return the same block at height 2 while one provider's finalized head points to a different parent. A bridge indexer that compares only the height and hash of that numeric block would miss the contradiction. The downloadable Python lab reproduces this case with four synthetic blocks, then rejects it by walking back from each tagged head.

You will also test whether a deposit log actually appears in its transaction receipt. The result is a small, runnable evidence checker with separate outcomes for missing data and contradictory data. It uses Python's standard library and runs entirely offline. Every block, address and transaction identifier is a teaching fixture.

I checked Passage Bridge's scanner and local relayer source when preparing this example. The scanner follows parent identities from tagged heads, checks receipts against logs, and commits indexed facts with their projections in a database transaction. Those are useful engineering choices to examine. This independent Python program demonstrates a subset of those checks; it does not validate a live chain or authorize a payout.

Download the complete bridge indexer lab. It includes the implementation, 14 tests and instructions.

Start with the disagreement you want to catch

An RPC response for a finalized tag supplies an anchor for subsequent checks. If providers report different finalized heights, choose the lower height and follow each provider's own parent links back to it. The comparison now concerns blocks connected to the reported heads.

The lab deliberately supplies symbolic hashes such as block-2. Production block hashes have different encoding and verification requirements. Here the strings make the failing relationship easy to read.

from dataclasses import replace
from finality import fixture, agreed_anchor, Conflict

chain, log, receipt = fixture()
fork = dict(chain)
fork[3] = replace(chain[3], parent="other-block-2")

try:
    agreed_anchor(fork, fork[3], chain, chain[2])
except Conflict as error:
    print(error)  # tagged branch changed

Both dictionaries still contain the identical object at numeric height 2. The primary head says its parent is other-block-2, so the walk rejects the supplied numeric block. This is the exact distinction that an equal-height comparison would lose. The test test_agreeing_numeric_blocks_cannot_hide_wrong_parent makes the regression reproducible.

Passage's inspected scanner uses this parent-walk approach when comparing tagged RPC heads. It also bounds the walk. Its local confirmation mode follows a separate policy, so a successful local run should never be described as evidence that the production finality policy has passed.

Run the archive in a clean directory

Download and extract the ZIP into an empty directory. No package installation, wallet or RPC credentials are required. The code targets Python 3.10 or newer; the execution log supplied with this draft records the interpreter actually used.

python3 -m unittest -v
python3 finality.py

The first command runs 14 tests. The second accepts a receipt-backed synthetic event at block 1 under an agreed anchor at height 2, then prints a rejection for the deliberately broken parent link. The tests were executed after extracting the final ZIP into a fresh temporary directory.

This separation makes the download reviewable. A buyer can run the same artifact a developer receives, and a developer can change a fixture without setting up the product stack. The complete product's historical test counts are outside this lab's result.

Bound the walk and preserve the reason for waiting

ancestor starts with the supplied head hash, loads the block at that height, and checks the expected identity. It then carries the parent hash into the next iteration. The downloadable implementation uses this loop.

expected = head.hash
for number in range(head.number, height - 1, -1):
    block = blocks.get(number)
    if block is None:
        raise Unavailable("missing ancestor")
    if block.number != number or block.hash != expected:
        raise Conflict("tagged branch changed")
    if number == height:
        return block
    expected = block.parent

The caller sets max_steps. A distance outside that bound raises Unavailable, as does a missing block. A mismatched parent identity raises Conflict. Neither outcome accepts the event, but the distinction gives an operations workflow useful information. Missing data can justify fetching a wider batch or retrying later. Conflicting identities require investigation of the evidence already supplied.

The lab does not schedule retries. It exposes those reasons to the caller so that retry policy can be tested separately. A large walk bound also has a cost in a real RPC adapter. It increases the number of block requests and the time during which responses can change. The product scanner includes an additional reread of fetched block identities before committing; this offline model assumes its dictionaries remain fixed during a call.

Compare the parent paths from two tagged RPC heads, then verify a matching receipt and full log before accepting a synthetic source fact.
Teaching model of evidence admission; destination approval and payout are separate.

Check the event against the receipt

Agreement about a block does not show that the claimed deposit log occurred in it. The next check needs the successful receipt, its transaction identity and its actual log entries. In the lab, the full frozen Log value must appear in receipt.logs.

result = accept_log(
    chain, chain[3], chain, chain[2],
    log, receipt, expected_address="pool-demo",
)
assert result["decision"] == "accepted"
assert result["block_hash"] == "block-1"

The function checks the expected contract address and rejects removed logs. It compares the receipt's block number, block hash and transaction hash with the event evidence, and requires a success status. Changing only the receipt log's data, topic or index makes the test fail. A transaction receipt with the right hash but an unrelated event therefore cannot satisfy this checker.

In Passage's source, receiptMatches performs corresponding identity and log-content comparisons. The scanner also rejects logs from the wrong target or block, deduplicates them by block hash, transaction hash and log index, and orders the retained events before projection. The teaching program stops at one accepted fact. It leaves deduplication, database commits and projection recovery to the integration that follows.

Turn the test into a delivery discussion

For a wallet or multichain application buyer, the practical question is how an unresolved source fact reaches the user interface and operations team. An accepted fact here means that the supplied observations passed these checks. It carries no approval to spend destination inventory. Passage's inspected local relayer separately requires finalized source and approval states, indexed data, and an unclaimed execution state before continuing with its policy checks.

Provider agreement still depends on the providers and the chain policy. Two providers can share infrastructure or return the same incorrect information. This lab contains no light client, consensus verification, cryptographic block validation or independent deposit proof. Its exact string comparisons also assume inputs have already been normalized by an adapter.

The Passage Bridge integration scope is the appropriate place to agree on target networks and evidence policy. The current product uses independent operational approval and destination inventory. Its relayer signing path rejects non-local execution; production signing requires additional implementation and acceptance.

Ask for a demonstration where one provider is missing data and another where the two branches conflict. Then inspect the recorded reason and confirm that neither case advances to payout. The earlier message identity article covers how an approved message binds its transfer fields. This lab gives that discussion an earlier checkpoint with runnable failures and inspectable evidence.