Skip to content

Reading the anchor on-chain yourself

06 · On-chain

The point of anchoring on a public chain is that you never have to take Backrun's word for what's there. This page shows exactly how to look it up — with a public RPC endpoint, no account, no API key, and no Backrun software involved at any step.

The contract

Address0x256080339DEA7E8F3089C49CCEB0F98547103f91
ChainBase mainnet, chain id 8453
Explorerbasescan.org
Sourcecontracts/src/AuditAnchor.sol, MIT licensed

Confirm the contract and chainId fields on your own receipt match these before trusting anything else on this page — a root that matches on the wrong chain or the wrong contract proves nothing about the right one.

Why this contract can be trusted to not have a back door

The entire contract, in full — short enough to read end to end:

contract AuditAnchor {
    event Anchored(
        address indexed publisher,
        uint256 indexed sequence,
        bytes32 indexed root,
        bytes32 batchId,
        uint256 timestamp
    );

    mapping(address publisher => uint256 count) public anchorCount;

    error EmptyRoot();

    function anchor(bytes32 root, bytes32 batchId) external returns (uint256 sequence) {
        if (root == bytes32(0)) revert EmptyRoot();
        unchecked { sequence = ++anchorCount[msg.sender]; }
        emit Anchored(msg.sender, sequence, root, batchId, block.timestamp);
    }
}

That's the whole contract. Notice what's absent:

  • No owner, admin role, or access control of any kind — anyone can call anchor(), for their own address only.
  • No upgradeability — no proxy, no delegatecall, no way to swap in different logic later.
  • No function that updates or deletes a past anchor. There is no code path for it — not "disabled," structurally absent.
  • No pause switch.

Roots live in the event log, not contract storage — logs are far cheaper than SSTORE and equally readable by anyone with an archive node or indexer. The only thing actually stored is anchorCount, a per-publisher counter, so a verifier can detect gaps: if you expect sequence 1 through N and only find 1 through N−1, something is missing, without needing an indexer to tell you so.

Anchors are namespaced by msg.sender. Backrun's operator key is not a privileged authority in this contract — it's one publisher among any number of others who could call the same function. A customer who stopped trusting Backrun entirely could anchor their own roots on this same contract and keep every property this page describes.

The Anchored event

event Anchored(
  address indexed publisher,
  uint256 indexed sequence,
  bytes32 indexed root,
  bytes32 batchId,
  uint256 timestamp
)

Solidity ABI (JSON), matching engine/src/anchor/auditAnchorAbi.ts:

{
  "type": "event",
  "name": "Anchored",
  "inputs": [
    { "name": "publisher", "type": "address", "indexed": true },
    { "name": "sequence",  "type": "uint256", "indexed": true },
    { "name": "root",      "type": "bytes32", "indexed": true },
    { "name": "batchId",   "type": "bytes32", "indexed": false },
    { "name": "timestamp", "type": "uint256", "indexed": false }
  ]
}

Topic 0 (the event signature hash every log of this type carries) is keccak256("Anchored(address,uint256,bytes32,bytes32,uint256)"):

0x8277244955aa743047a5b8241ef1294e25c28ff1b2a98dcf10710a67510c43b5

Finding your root with cast

Foundry's cast against any public Base RPC — no key, no signup:

# current chain tip, to sanity-check you're pointed at Base mainnet
cast chain-id --rpc-url https://mainnet.base.org
# → 8453

# how many times a given publisher has anchored (your receipt's `publisher` field)
cast call 0x256080339DEA7E8F3089C49CCEB0F98547103f91 \
  "anchorCount(address)(uint256)" 0x16EF29F63A4CB0624EF5c1079E08E1530A0b7142 \
  --rpc-url https://mainnet.base.org
# → 2

# the actual Anchored logs — eth_getLogs is capped at a 10,000-block range per call
# by most public RPCs, so widen fromBlock/toBlock if your anchor is further back
cast logs --rpc-url https://mainnet.base.org \
  --address 0x256080339DEA7E8F3089C49CCEB0F98547103f91 \
  --from-block 49840000 --to-block latest \
  "Anchored(address,uint256,bytes32,bytes32,uint256)"

Real output, from this contract, at the time this page was written:

- address: 0x256080339DEA7E8F3089C49CCEB0F98547103f91
  blockNumber: 49844752
  data: 0xe1f0680050984951b9bd7f250e75441800000000000000000000000000000000
        000000000000000000000000000000000000000000000000000000006a7b8103
  topics: [
    0x8277244955aa743047a5b8241ef1294e25c28ff1b2a98dcf10710a67510c43b5  // topic0: Anchored(...)
    0x00000000000000000000000016ef29f63a4cb0624ef5c1079e08e1530a0b7142  // publisher
    0x0000000000000000000000000000000000000000000000000000000000000001  // sequence = 1
    0x9c84221263e9265b0e2e62bd356393fa6f30c01339e26f21ab645da2da652195  // root
  ]
  transactionHash: 0xa6f5999326a7e32173afb7b1d660830103254b11f63b9656bf41d1b6cf262f9a

- address: 0x256080339DEA7E8F3089C49CCEB0F98547103f91
  blockNumber: 49844809
  data: 0x7ec21c87385800d6a85a16baa6c044c948e42edb5520ba8c204328cf2013e6b6
        00000000000000000000000000000000000000000000000000000006a7b8175
  topics: [
    0x8277244955aa743047a5b8241ef1294e25c28ff1b2a98dcf10710a67510c43b5
    0x00000000000000000000000016ef29f63a4cb0624ef5c1079e08e1530a0b7142
    0x0000000000000000000000000000000000000000000000000000000000000002  // sequence = 2
    0xf08778f618913874b8e07513f7abfbb9457d911db3d501a0d394096ec487f0b0
  ]
  transactionHash: 0x806421299727b8524c7fa68e86f9b8c1b0209bb82ad47f37d09112da4fd1ca33

data holds the two non-indexed parameters back to back, 32 bytes each: batchId then timestamp. Decoded, the first log above is batchId = 0xe1f0680050984951b9bd7f250e75441800000000000000000000000000000000, timestamp = 1786478851 (2026-08-11T20:07:31Z) — matching the batchId/anchoredAt pair on a real receipt exactly.

Once you have the root from a log, compare it — as a plain string — to receipt.root. That comparison, plus the offline proof fold described on the verification algorithm page, is the entire trust model.

Finding your root with viem

import { createPublicClient, http, parseAbiItem } from "viem";
import { base } from "viem/chains";

const client = createPublicClient({ chain: base, transport: http() }); // public RPC, no key

const logs = await client.getLogs({
  address: "0x256080339DEA7E8F3089C49CCEB0F98547103f91",
  event: parseAbiItem(
    "event Anchored(address indexed publisher, uint256 indexed sequence, bytes32 indexed root, bytes32 batchId, uint256 timestamp)"
  ),
  args: { publisher: "0x16EF29F63A4CB0624EF5c1079E08E1530A0b7142" }, // your receipt's `publisher` field
  fromBlock: 49_840_000n,
  toBlock: "latest",
});

for (const log of logs) {
  console.log(log.args.sequence, log.args.root, log.args.batchId, log.args.timestamp);
}

This is the same call the engine's own auditAnchorAbi (engine/src/anchor/auditAnchorAbi.ts) is built for — kept hand-written rather than generated specifically so it stays readable by a human without a build step, since it's the interface a third-party verifier reads, not just internal plumbing.