For the complete documentation index, see llms.txt. This page is also available as Markdown.

BrainAuditAnchor

BrainAuditAnchor stores Merkle roots of per-tenant audit batches. Anchors are immutable after submission.

Property
Value

Network

Base Sepolia only

Solidity

0.8.x

Pattern

Immutable. No upgrade path in MVP; changes require a redeploy.

Audit status

Unaudited. Base mainnet is fenced pending an external smart-contract audit.

Publisher

Single EOA at 0x41d4ce9d9fe968ca1230bdc296b28fdc9aa6ff6e, verified on Base Sepolia.

Interface

interface IBrainAuditAnchor {
    event AnchorPublished(
        bytes32 indexed tenantId,
        bytes32 root,
        uint256 eventCount,
        uint256 periodStart,
        uint256 periodEnd
    );

    function anchor(
        bytes32 tenantId,
        bytes32 root,
        uint256 eventCount,
        uint256 periodStart,
        uint256 periodEnd
    ) external;  // onlyPublisher

    function anchorBatch(
        bytes32[] calldata tenantIds,
        bytes32[] calldata roots,
        uint256[] calldata eventCounts,
        uint256[] calldata periodStarts,
        uint256[] calldata periodEnds
    ) external;  // onlyPublisher, maximum 50 entries

    function publisher() external view returns (address);

    function MAX_BATCH() external view returns (uint256);

    function latestAnchor(bytes32 tenantId)
        external view returns (bytes32 root, uint256 blockNumber);

    function latestAnchorFull(bytes32 tenantId)
        external view
        returns (bytes32 root, uint256 blockNumber, uint256 eventCount, uint256 periodEnd);

    function verifyInclusion(
        bytes32 root,
        bytes32 leaf,
        bytes32[] calldata proof
    ) external pure returns (bool);

    function isPublished(bytes32 tenantId, bytes32 root)
        external view returns (bool);
}

Publication is authorized by the caller, not by a per-call signature. anchor and anchorBatch are onlyPublisher. The current Base Sepolia publisher is a single EOA, not a Safe multisig. setPublisher and acceptPublisher provide a two-step rotation path.

How Anchoring Works

Step
Detail

1

Audit events batch into a Merkle tree per tenant over a period window

2

The publisher submits root, event count, and period bounds

3

anchor() publishes one root, or anchorBatch() publishes up to MAX_BATCH roots

4

Contract emits AnchorPublished; the root becomes immutably retrievable

Replay Protection

The contract records every published (tenantId, root) pair and rejects a repeat.

Behavior
Detail

First time a root is published

Stored as the tenant's latest anchor

Re-publishing the same root

Reverts with RootAlreadyPublished

Period bounds

periodEnd before periodStart reverts

Root-uniqueness per tenant is the replay guard: a published root cannot be re-anchored for the same tenant. There is no batch-index sequence to maintain, so anchoring never depends on submission order.

anchorBatch() has the same period validation as anchor() and a hard MAX_BATCH cap of 50. Unlike single-root anchor(), it skips an already published (tenantId, root) pair. This makes a batch retry safe after a prior partial success.

Verification by Counterparties

A counterparty does not need a RobotMoney account to verify an audit event. They just need:

Input
Source

root

The published Merkle root (read via latestAnchor or event logs)

leaf

Hash of the event being verified

proof[]

Merkle path supplied by RobotMoney

If valid is true, the event is provably part of the anchored history under that root. verifyInclusion uses domain-separated hashing: leaf nodes are keccak256(0x00 ++ leaf) and internal nodes are keccak256(0x01 ++ sort(left, right)).

Reorg Tolerance

Base L2 has fast finality, but small reorgs are possible.

Mitigation
Detail

Confirmation depth

Reads wait for a configurable depth before treating an anchor as final

Retryable publication

The publisher retains pending anchors and retries them after transient failures

Off-chain log canonical until anchored

A record remains pending until its on-chain transaction is confirmed

Publisher Rotation

The publisher is rotated through a two-step handoff so a mistyped or uncontrolled address can never brick anchoring. The current publisher proposes the next address with setPublisher(next) (publisher-only), and the rotation takes effect only when that address calls acceptPublisher(). The contract itself is immutable, so there is no upgrade path. Only the publisher address changes.

Privacy

Only Merkle roots and hashed tenantId values are on-chain. Everything underneath stays off-chain in tenant-prefixed storage and tenant-scoped database rows. Source credentials use the global AES-256-GCM credential key described in shared/src/crypto/credential-key-provider.ts.

On-chain
Off-chain

tenantId (hashed)

Tenant raw identifier

root (Merkle root)

Individual audit events

eventCount, periodStart, periodEnd

Event content, citations, decisions

What's Next

Audit and Proof

The conceptual model.

Audit API

Retrieve events and proofs over HTTP.

Last updated