Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/CURRENT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ FlowMemory is in launch-candidate V0 hardening.

The bootstrap repository operating system, contracts V0 foundation, crypto V0 foundation, local indexer/verifier fixture package, dashboard V0, FlowRouter hardware POC, local no-value devnet prototype, launch-core contract-event spine, and pre-production hardening guardrails have merged into `main`. The launch-candidate work added swap-derived memory signals, stricter launch validation, and Base Sepolia testnet deploy/read commands.

On 2026-05-13 a small Base mainnet canary deployment was broadcast for V0 testing. It is documented in `docs/DEPLOYMENTS/2026-05-13-base-canary-v0.md`. This is not a production launch and does not change the production/mainnet-readiness guardrails.
On 2026-05-13 a small Base mainnet canary deployment was broadcast for V0 testing. It is documented in `docs/DEPLOYMENTS/2026-05-13-base-canary-v0.md`. A guarded Base mainnet canary reader now exists for those known canary addresses and small explicit block ranges. This is not a production launch and does not change the production/mainnet-readiness guardrails.

The launch-core V0 stack now has a single runnable local command that connects contract fixtures, local indexing/verifier outputs, crypto schema vocabulary, Rootflow transitions, Flow Memory objects, generated dashboard state, local no-value devnet output, and hardware POC output without production deployment.

Expand Down Expand Up @@ -55,6 +55,9 @@ Indexer/verifier local package:
- The verifier supports local fixture checks for rootfield registration, root commitments, and swap-derived memory-signal commitments.
- `npm run index:base-sepolia -- --rpc-url <url> --address <contract> --from-block <n> --to-block <n>` provides a constrained Base Sepolia reader path.
- The Base Sepolia reader requires an explicit RPC URL, rejects non-Base-Sepolia chain ids, and persists both canonical state and a durable checkpoint without storing RPC URLs or keys.
- `npm run index:base-canary -- --acknowledge-mainnet-canary --rpc-url <url> --address <contract> --from-block <n> --to-block <n>` provides a guarded Base mainnet canary reader path for the documented V0 canary deployment only.
- The Base canary reader requires explicit acknowledgement, RPC URL, addresses, and block range; rejects non-Base-mainnet chain ids; refuses scans wider than 5,000 blocks; persists canonical state plus a durable canary checkpoint; and marks the checkpoint as not production-ready.
- A live canary read over blocks `45955500` to `45955540` observed 4 FlowPulse logs from the documented `RootfieldRegistry` and `FlowMemoryHookAdapter` canary addresses with 0 rejected logs and 0 duplicates.
- `npm run deploy:base-sepolia` and `npm run deploy:base-sepolia:broadcast` provide Foundry deploy commands for the current V0 Base Sepolia testnet contract set. They require local env values and do not commit credentials.
- A Base mainnet V0 canary deployment exists for testing only; deployed addresses and smoke transactions are recorded in `docs/DEPLOYMENTS/2026-05-13-base-canary-v0.md`.

Expand Down Expand Up @@ -114,7 +117,7 @@ Launch-core specifications:
- Hosted launch-core services.
- Production indexer or verifier service runtime.
- Production persistence layer, production live RPC reader, production APIs, or hosted services.
- Base mainnet reader.
- Broad Base mainnet reader.
- Dashboard ingestion of the Base mainnet canary deployment.
- Contract source verification automation for the deployed canary contracts.
- Explorer or hardware console implementation.
Expand Down Expand Up @@ -176,7 +179,7 @@ Before assigning agents, check for dirty worktrees and avoid overlapping folders
## Current Operator Priorities

1. Keep the generated launch-core command stable in CI.
2. Build a guarded Base canary reader path for the deployed V0 canary addresses.
2. Exercise the guarded Base canary reader against the documented V0 canary addresses and feed its output into the next dashboard canary-ingestion issue.
3. Exercise the Base Sepolia deploy/read path on explicit testnet contract addresses only.
4. Continue contracts hardening without production mainnet deployment or token mechanics.
5. Keep dashboard work fixture-backed until a production API is explicitly scoped.
Expand Down
50 changes: 42 additions & 8 deletions docs/DEPLOYMENTS/2026-05-13-base-canary-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ Rootfield id:
- Commitment:
`0x30055afe075a7c6ea8557ea3a2d3c7012d9d558ebda95803726179355f98ede9`

### Additional Swap-Memory Signal Observed By Reader

The guarded canary reader also observed an earlier swap-memory signal emitted
by the hook adapter during smoke testing:

- Transaction: `0x5f81dc48c5d172ff3f44a333a33598f23c82be2614f4156d5dd3257a16806cc7`
- Block: `45955507`
- FlowPulse pulse id: `0x2d436d766f9777b7f9925d57d8b2d57def3fdfae405017104f21795e20eacef7`
- Pulse type: `4` / `SWAP_MEMORY_SIGNAL`
- Commitment:
`0x30055afe075a7c6ea8557ea3a2d3c7012d9d558ebda95803726179355f98ede9`

## State Readback

`RootfieldRegistry.getRootfield(rootfieldId)` returned:
Expand All @@ -84,19 +96,41 @@ rootCount: 1
active: true
```

## Guarded Reader Command

The repo now includes a guarded canary reader for these live V0 canary logs. It
requires explicit acknowledgement, explicit addresses, and a small explicit
block range:

```powershell
npm run index:base-canary -- --acknowledge-mainnet-canary --rpc-url https://mainnet.base.org --address 0x2a7ADd68a1d45C3251E2F92fFe4926124654a97C --address 0x179Df6d52e9DeF5D02704583a2E4E5a9FF427245 --from-block 45955500 --to-block 45955540 --finalized-block 45955540
```

Observed canary smoke read for the range above:

- `RootfieldRegistry`: rootfield registration and root submission FlowPulse logs.
- `FlowMemoryHookAdapter`: two swap-memory signal FlowPulse logs.
- Observation count: `4`.
- Rejected log count: `0`.
- Duplicate count: `0`.
- Last indexed block: `45955535`.
- Output state: `services/indexer/out/base-canary-indexer-state.json` by default.
- Output checkpoint: `services/indexer/out/base-canary-indexer-checkpoint.json` by default.

The canary reader refuses non-Base-mainnet RPC endpoints, refuses scans wider
than 5,000 blocks, stores no RPC URLs or private keys, and marks checkpoint
output as not production-ready.

## Important Gaps Found

1. The checked-in live reader is Base Sepolia-only and intentionally rejects
Base mainnet chain id `8453`. A guarded Base canary reader is needed before
the dashboard can ingest live mainnet canary logs.
2. The dashboard still consumes generated fixtures. It does not yet ingest a
1. The dashboard still consumes generated fixtures. It does not yet ingest a
deployment artifact plus live read output.
3. Contract source verification is not automated for all deployed contracts.
4. `FlowMemoryHookAdapter` is still an adapter scaffold. It is not a production
2. Contract source verification is not automated for all deployed contracts.
3. `FlowMemoryHookAdapter` is still an adapter scaffold. It is not a production
Uniswap v4 hook wired into PoolManager permissions.
5. Ownership is still direct deployer ownership where applicable. There is no
4. Ownership is still direct deployer ownership where applicable. There is no
multisig, governance, recovery, or operational key policy.
6. Verifier and worker registry flows are deployed, but live verifier report
5. Verifier and worker registry flows are deployed, but live verifier report
submission, report signing, and verifier economics are not built.

## Notes
Expand Down
5 changes: 3 additions & 2 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ Status: implemented as fixture-first services plus generated launch-core state;
- Flow Memory schemas for MemorySignal, MemoryReceipt, RootfieldBundle, and AgentMemoryView exist under `schemas/flowmemory/`.
- Generated MemorySignal and RootflowTransition fixtures expose contract-event linkage through `contractEvent` and `contractEventRef`.
- Fixture-based parser and reorg-state tests exist in the indexer/verifier packages.
- Deterministic persistence exists for fixture state and the constrained Base Sepolia reader checkpoint.
- Deterministic persistence exists for fixture state, the constrained Base Sepolia reader checkpoint, and the guarded Base mainnet canary checkpoint.
- A Base Sepolia reader path exists for explicit RPC URLs and explicit FlowPulse contract addresses; it rejects non-Base-Sepolia chain ids.
- A guarded Base mainnet canary reader exists for explicit RPC URLs, explicit known canary addresses, and small explicit block ranges; it rejects non-Base-mainnet chain ids and marks output as canary-only.
- Base Sepolia deploy/read commands exist for the current V0 testnet contract set.
- A Base mainnet V0 canary deployment has been performed for testing only and is documented under `docs/DEPLOYMENTS/`.
- Runtime schema validation and generated fixture drift checks exist for launch-core outputs.
Expand Down Expand Up @@ -142,7 +143,7 @@ The initial merge sequence has completed for repo OS, contracts foundation, cryp

Next merge preference:

1. Guarded Base canary reader and deployment-artifact ingestion.
1. Deployment-artifact ingestion for the guarded Base canary reader output.
2. Base Sepolia reader soak tests against explicit testnet deployments.
3. Dashboard live/canary mode separation from generated fixtures.
4. Static analysis follow-up findings triaged for any public testnet deployment.
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"test": "npm test --prefix services/shared && npm test --prefix services/indexer && npm test --prefix services/verifier && npm test --prefix services/flowmemory",
"contracts:hardening": "node infra/scripts/run-contract-hardening.mjs",
"contracts:hardening:slither": "node infra/scripts/run-contract-hardening.mjs --require-slither",
"index:base-canary": "npm run index:base-canary --prefix services/indexer",
"read:base-canary": "npm run index:base-canary --prefix services/indexer",
"index:base-sepolia": "npm run index:base-sepolia --prefix services/indexer",
"read:base-sepolia": "npm run index:base-sepolia --prefix services/indexer",
"deploy:base-sepolia": "node infra/scripts/run-base-sepolia-deploy.mjs",
Expand Down
11 changes: 11 additions & 0 deletions services/indexer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ From the repository root:
```powershell
npm run index:fixtures
npm run index:base-sepolia -- --rpc-url <base-sepolia-rpc-url> --address <flowpulse-contract> --from-block <n> --to-block <n>
npm run index:base-canary -- --acknowledge-mainnet-canary --rpc-url <base-mainnet-rpc-url> --address <canary-flowpulse-contract> --from-block <n> --to-block <n>
npm run demo:indexer
npm test --prefix services/indexer
```
Expand Down Expand Up @@ -177,6 +178,14 @@ flowmemory.indexer.base_sepolia_checkpoint.v0

The checkpoint records the network, chain id, emitting addresses, scan range, finality threshold, state path, counts, and latest indexed block. It intentionally does not store RPC URLs or private keys.

The Base mainnet canary reader writes:

```text
flowmemory.indexer.base_canary_checkpoint.v0
```

It is for the documented V0 canary deployment only. It requires `--acknowledge-mainnet-canary`, an explicit RPC URL, explicit emitting addresses, and an explicit block range. It rejects non-Base-mainnet endpoints and refuses scans wider than 5,000 blocks. The checkpoint marks `productionReady: false` and intentionally does not store RPC URLs or private keys.

The JSON schema fixture lives at:

```text
Expand All @@ -189,4 +198,6 @@ services/indexer/fixtures/indexer-state.schema.json

`readBaseSepoliaFlowPulseLogs` is the current live reader boundary. It requires an explicit RPC URL and refuses endpoints unless `eth_chainId` returns Base Sepolia (`84532`). It is not a Base mainnet reader and does not make production-mainnet readiness claims.

`readBaseMainnetCanaryFlowPulseLogs` is the narrow Base mainnet canary boundary. It requires an explicit RPC URL and refuses endpoints unless `eth_chainId` returns Base mainnet (`8453`). It is not a broad production indexer and should only be used against known canary contract addresses and small block ranges.

See [docs/INDEXER_VERIFIER_MVP.md](../../docs/INDEXER_VERIFIER_MVP.md) for the full pipeline.
1 change: 1 addition & 0 deletions services/indexer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"type": "module",
"scripts": {
"demo": "node src/demo.ts",
"index:base-canary": "node src/base-canary.ts",
"index:base-sepolia": "node src/base-sepolia.ts",
"index:fixtures": "node src/index-fixtures.ts",
"test": "node --test test/*.test.ts"
Expand Down
215 changes: 215 additions & 0 deletions services/indexer/src/base-canary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { resolve } from "node:path";

import { indexFlowPulseLogs, type IndexerState } from "./indexer.ts";
import {
baseCanaryIndexerCheckpoint,
type BaseCanaryIndexerCheckpoint,
writeBaseCanaryIndexerCheckpoint,
writeIndexerState,
} from "./persistence.ts";
import {
blockArgumentToDecimalString,
blockArgumentToRpcQuantity,
normalizeEvmAddresses,
readArgValue,
} from "./reader-utils.ts";
import { BASE_MAINNET_CHAIN_ID, readBaseMainnetCanaryFlowPulseLogs } from "./rpc.ts";

export const BASE_CANARY_MAX_BLOCK_SPAN = 5_000n;

export interface BaseCanaryReaderOptions {
rpcUrl: string;
addresses: string[];
fromBlock: string;
toBlock: string;
outPath?: string;
checkpointPath?: string;
finalizedBlockNumber?: string;
generatedAt?: string;
acknowledgeMainnetCanary?: boolean;
fetchImpl?: typeof fetch;
}

export interface BaseCanaryReaderResult {
state: IndexerState;
checkpoint: BaseCanaryIndexerCheckpoint;
statePath: string;
checkpointPath: string;
}

interface CliOptions extends BaseCanaryReaderOptions {
outPath: string;
checkpointPath: string;
acknowledgeMainnetCanary: true;
}

function assertCanaryAcknowledged(acknowledgeMainnetCanary?: boolean): void {
if (acknowledgeMainnetCanary !== true) {
throw new Error("--acknowledge-mainnet-canary is required for the Base mainnet canary reader");
}
}

function assertCanaryBlockRange(fromBlock: string, toBlock: string): void {
if (BigInt(toBlock) < BigInt(fromBlock)) {
throw new Error("--to-block must be greater than or equal to --from-block");
}

const span = BigInt(toBlock) - BigInt(fromBlock);
if (span > BASE_CANARY_MAX_BLOCK_SPAN) {
throw new Error(
`Base canary reader refuses broad scans; block span ${span.toString()} exceeds ${BASE_CANARY_MAX_BLOCK_SPAN.toString()}`,
);
}
}

export function parseBaseCanaryReaderArgs(args: string[]): CliOptions {
let rpcUrl = "";
let fromBlock = "";
let toBlock = "";
let finalizedBlockNumber: string | undefined;
let acknowledgeMainnetCanary = false;
const addresses: string[] = [];
let outPath = "out/base-canary-indexer-state.json";
let checkpointPath = "out/base-canary-indexer-checkpoint.json";

for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--acknowledge-mainnet-canary") {
acknowledgeMainnetCanary = true;
} else if (arg === "--rpc-url") {
rpcUrl = readArgValue(args, index, arg);
index += 1;
} else if (arg === "--address" || arg === "--addresses") {
addresses.push(readArgValue(args, index, arg));
index += 1;
} else if (arg === "--from-block") {
fromBlock = readArgValue(args, index, arg);
index += 1;
} else if (arg === "--to-block") {
toBlock = readArgValue(args, index, arg);
index += 1;
} else if (arg === "--finalized-block") {
finalizedBlockNumber = blockArgumentToDecimalString(readArgValue(args, index, arg));
index += 1;
} else if (arg === "--out") {
outPath = readArgValue(args, index, arg);
index += 1;
} else if (arg === "--checkpoint-out") {
checkpointPath = readArgValue(args, index, arg);
index += 1;
} else {
throw new Error(`unknown argument: ${arg}`);
}
}

if (rpcUrl.trim() === "") {
throw new Error("--rpc-url is required; FlowMemory does not ship a default RPC endpoint");
}
if (fromBlock.trim() === "") {
throw new Error("--from-block is required");
}
if (toBlock.trim() === "") {
throw new Error("--to-block is required");
}

assertCanaryAcknowledged(acknowledgeMainnetCanary);

const normalizedFromBlock = blockArgumentToDecimalString(fromBlock);
const normalizedToBlock = blockArgumentToDecimalString(toBlock);
assertCanaryBlockRange(normalizedFromBlock, normalizedToBlock);

return {
rpcUrl,
addresses: normalizeEvmAddresses(addresses),
fromBlock: normalizedFromBlock,
toBlock: normalizedToBlock,
finalizedBlockNumber,
outPath,
checkpointPath,
acknowledgeMainnetCanary: true,
};
}

export async function runBaseCanaryReader(options: BaseCanaryReaderOptions): Promise<BaseCanaryReaderResult> {
assertCanaryAcknowledged(options.acknowledgeMainnetCanary);

const addresses = normalizeEvmAddresses(options.addresses);
const fromBlock = blockArgumentToDecimalString(options.fromBlock);
const toBlock = blockArgumentToDecimalString(options.toBlock);
const outPath = resolve(options.outPath ?? "out/base-canary-indexer-state.json");
const checkpointPath = resolve(options.checkpointPath ?? "out/base-canary-indexer-checkpoint.json");

assertCanaryBlockRange(fromBlock, toBlock);

const readResult = await readBaseMainnetCanaryFlowPulseLogs({
rpcUrl: options.rpcUrl,
addresses,
fromBlock: blockArgumentToRpcQuantity(fromBlock),
toBlock: blockArgumentToRpcQuantity(toBlock),
fetchImpl: options.fetchImpl,
});

const finalizedBlockNumber = options.finalizedBlockNumber === undefined
? undefined
: blockArgumentToDecimalString(options.finalizedBlockNumber);

const state = indexFlowPulseLogs(readResult.logs, {
chainId: BASE_MAINNET_CHAIN_ID,
finalizedBlockNumber,
source: "base-mainnet-canary-rpc",
sourceAddresses: addresses,
});
const checkpoint = baseCanaryIndexerCheckpoint({
addresses,
fromBlock,
toBlock,
finalizedBlockNumber,
statePath: outPath,
state,
generatedAt: options.generatedAt,
});

writeIndexerState(outPath, state);
writeBaseCanaryIndexerCheckpoint(checkpointPath, checkpoint);

return {
state,
checkpoint,
statePath: outPath,
checkpointPath,
};
}

function usage(): string {
return [
"Usage:",
" node src/base-canary.ts --acknowledge-mainnet-canary --rpc-url <url> --address <0x...> --from-block <n> --to-block <n> [--finalized-block <n>] [--out <path>] [--checkpoint-out <path>]",
"",
"Boundary:",
` This reader only accepts Base mainnet chainId ${BASE_MAINNET_CHAIN_ID} and is canary-only.`,
` It refuses scans wider than ${BASE_CANARY_MAX_BLOCK_SPAN.toString()} blocks and stores no RPC URLs or keys.`,
].join("\n");
}

if (process.argv[1]?.replaceAll("\\", "/").endsWith("/base-canary.ts")) {
runBaseCanaryReader(parseBaseCanaryReaderArgs(process.argv.slice(2)))
.then((result) => {
console.log(JSON.stringify({
schema: "flowmemory.indexer.base_canary_reader_summary.v0",
network: result.checkpoint.network,
chainId: result.checkpoint.chainId,
statePath: result.statePath,
checkpointPath: result.checkpointPath,
observationCount: result.checkpoint.observationCount,
rejectedLogCount: result.checkpoint.rejectedLogCount,
duplicateCount: result.checkpoint.duplicateCount,
lastIndexedBlock: result.checkpoint.lastIndexedBlock,
productionReady: result.checkpoint.safety.productionReady,
}, null, 2));
})
.catch((error) => {
console.error(error instanceof Error ? error.message : error);
console.error(usage());
process.exitCode = 1;
});
}
Loading
Loading