Skip to content

refactor: chain config and registry as git-tracked data with API sync, drift check, and chain doctor#5

Open
aelmanaa wants to merge 1 commit into
mainfrom
refactor/chain-config-registry-api-sync
Open

refactor: chain config and registry as git-tracked data with API sync, drift check, and chain doctor#5
aelmanaa wants to merge 1 commit into
mainfrom
refactor/chain-config-registry-api-sync

Conversation

@aelmanaa

@aelmanaa aelmanaa commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What improves for the user

Before: chain metadata lives in hardcoded Solidity (HelperConfig), deployed addresses are manual export env vars, and adding a chain means editing source code. Keeping router/registry addresses current requires hand-editing constants with no drift detection.

After: chain config is git-tracked JSON under config/chains/, synced from the live CCIP API; deployed token/pool/lockbox addresses are recorded automatically in addresses/<chainId>.json and resolved by scripts without manual exports (env vars remain as overrides). Adding a chain is a config edit, not a code changemake discovermake add-chainmake doctor. The visible README command syntax is unchanged; the operational model is auditable, API-backed, and drift-aware.

Rebased onto main after #4 merged — this PR layers cleanly on top of the action layer and touches none of its files.

What this PR does

  1. config/chains/*.json — git-tracked per-chain CCIP metadata (router, registry, RMN, fee tokens, selectors, family). The file basename and lowercase name are the canonical CCIP selectorName from the chain-selectors registry (the same key the REST API, CLD, Atlas, the directory URL leaf, and ccip-cli share), e.g. ethereum-testnet-sepolia rather than a bespoke ethereum-sepolia. Six chains committed as examples; new chains are discovered automatically by scanning the directory.
  2. src/config/ChainConfig.sol + CcipApiSource.sol + IConfigSource.sol — typed config loading, API fetch/transform, and identity cross-checks. The sync joins on the numeric chainSelector (immutable machine key) and additionally validates that the config name equals the API selectorName. Unlike the chainId guard, the name check also covers non-EVM chains (whose config carries a placeholder chainId "0" the chainId check cannot verify) — closing that gap at add-chain time and in the doctor.
  3. script/HelperConfig.s.sol rewrite — dynamic chain discovery from config/chains/ instead of hardcoded per-chain getters; name/selector resolution now data-driven.
  4. addresses/<chainId>.json registrysrc/utils/RegistryWriter.sol records deploy outputs; deploy scripts (DeployToken, DeployBurnMintTokenPool, DeployLockReleaseTokenPool, DeployERC20LockBox, DeployAdvancedPoolHooks) write addresses automatically. Subsequent scripts resolve from registry; env vars are overrides only.
  5. script/config/SyncCcipConfig.s.sol + VerifyChain.s.sol — forge scripts behind sync (API → JSON) and chain doctor (layered verification: schema, identity, drift, RPC, on-chain code, registry warnings).
  6. Makefile golden pathdiscover, add-chain, sync, sync-preview, sync-all, sync-check, doctor, fmt-config wrapping the underlying forge script / bash commands documented in README.
  7. .github/workflows/config-drift.yml — scheduled (weekly) + workflow_dispatch drift check via script/config/sync-check.sh. Not a PR gate — surfaces upstream CCIP address rotations; exit contract 0 clean / 1 drift / 2 API unreachable (flake → warn-and-pass).
  8. Docs + README + .env.example — new docs/config-architecture.md (how config, sync, registry, and doctor fit together) and docs/config-schema.md (per-field schema reference), plus the README prerequisites table, "Adding a New Chain" guide, "Configuration and Address Data" section, and the Which command when table below.

Which command when

I want to... Run
See which chains exist / find a selector make discover FILTER=<term>
Onboard a new chain make add-chain CHAIN=<name> SELECTOR=<sel>, then make doctor CHAIN=<name>
Check whether any config drifted from the API (routine; what CI runs weekly) make sync-check (CI/automation: bash script/config/sync-check.sh for the 0/1/2 exit codes)
Inspect what the API currently has for one chain before changing anything make sync-preview CHAIN=<name>
Apply the API's current values make sync CHAIN=<name> / make sync-all
Deep-verify one chain end to end (human health check) make doctor CHAIN=<name>
Restore canonical formatting after a raw forge script run make fmt-config

doctor and sync-check layer rather than overlap: doctor is the deep single-chain health check for a human; sync-check is the fleet-wide drift verdict for routine use and CI.

Test coverage

New suite under test/config/ (27 tests):

  • HelperConfigGoldenData.t.sol (7) — config loading matches committed JSON
  • SyncFixtureTransform.t.sol (5) — API response → config JSON transform
  • SyncToolingRules.t.sol (5) — sync script invariants
  • RegistryResolution.t.sol (1) + RegistryGuard.t.sol (7) — address registry read/write/guard rules
  • DynamicChainDiscovery.t.sol (2) — new chains discovered from config/chains/ without Solidity edits

Acceptance checklist

  • make discover, make add-chain, make doctor golden path documented and exercised
  • Deploy scripts auto-record addresses to addresses/<chainId>.json
  • HelperConfig resolves chains dynamically from config/chains/
  • Config name/basename equals the CCIP API selectorName (validated for EVM and non-EVM chains)
  • make sync-check exit contract: 0 clean / 1 drift / 2 API unreachable
  • Committed chain configs canonical-formatted (jq --indent 2 -S .)
  • No README command syntax changed for existing flows (env vars now overrides, not required)
  • forge build, forge fmt --check, forge lint, forge test green

Test plan

  • forge build
  • forge fmt --check
  • forge lint
  • forge test
  • make sync-check (local)
  • make doctor CHAIN=ethereum-testnet-sepolia (local)

Out of scope: scripted lane-wiring golden path for ApplyChainUpdates (follow-up); action-layer rollout to configure/hooks/operations scripts (separate PR).

@aelmanaa aelmanaa requested review from a team as code owners July 7, 2026 22:28
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

👋 aelmanaa, thanks for creating this pull request!

To help reviewers, please consider creating future PRs as drafts first. This allows you to self-review and make any final changes before notifying the team.

Once you're ready, you can mark it as "Ready for review" to request feedback. Thanks!

@aelmanaa aelmanaa requested a review from SyedAsadKazmi July 7, 2026 22:29
@aelmanaa aelmanaa changed the title so yorefactor: chain config and registry as git-tracked data with API sync drift check, and chain doctor refactor: chain config and registry as git-tracked data with API sync, drift check, and chain doctor Jul 7, 2026
Comment thread config/chains/ethereum-sepolia.json Outdated
Comment thread script/config/test-tooling.sh Outdated
@aelmanaa aelmanaa force-pushed the refactor/chain-config-registry-api-sync branch 2 times, most recently from 5a20041 to da5f128 Compare July 8, 2026 23:16
@aelmanaa aelmanaa requested a review from SyedAsadKazmi July 8, 2026 23:18
@aelmanaa aelmanaa force-pushed the refactor/chain-config-registry-api-sync branch from da5f128 to 521ecda Compare July 9, 2026 00:00

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're already writing/storing deployed addresses in script/deployments folder which is gitignored, so can't we utilize that itself, instead of duplicating?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, it really was a double write. Each deploy script recorded the address twice on adjacent lines, once through DeploymentUtils.save* and again through the registry record. I've now folded both into a single DeploymentRecorder call, so it writes the ledger file (in your save* format, which stays untouched) and the registry entry together, and the two can no longer drift apart.

I did keep the registry as its own store rather than reading straight from script/deployments/, mainly because those files can't really act as the resolution source. Nothing reads them back since they're write only, they're gitignored so a fresh clone or a CI run starts with none of them, and they're keyed by chain name instead of the chainId that resolution relies on, with no marker for which entry is the live one. Both resolution and the redeploy guard need a single current state entry they can read, upsert, and delete, which append only timestamped files just can't give us.

…doctor, and a single-writer address registry

Chain metadata lives in git-tracked config/chains/<name>.json, read by
HelperConfig via ChainConfig.sol; the ccip{} address block is synced from
the CCIP REST API v2 with a field-by-field drift check and a layered
chain doctor.

The config file basename and lowercase `name` are the canonical CCIP
selectorName from the chain-selectors registry (the same key the REST API,
CLD, Atlas, the directory URL leaf, and ccip-cli all share), e.g.
ethereum-testnet-sepolia rather than the bespoke ethereum-sepolia. The sync
still joins on the numeric chainSelector (the immutable machine key), and now
also validates that the config name equals the API selectorName. Unlike the
chainId guard this works for non-EVM chains too, whose config carries a
placeholder chainId "0" the chainId check cannot verify — closing that gap
at add-chain time and in the doctor.

Unify the deployed-address write path and re-key the registry (schema v2).

Every deploy script previously wrote the same address twice, through two
adjacent, independently-maintained calls: DeploymentUtils.save* (the detailed
timestamped ledger) and RegistryWriter.record (the resolution registry). Two
stores, two formats, nothing keeping them in sync. Collapse them into a single
writer, script/utils/DeploymentRecorder.s.sol: one call per artifact emits the
ledger file (format byte-for-byte unchanged) AND upserts the registry, so the
two can no longer drift.

Reshape addresses/<chainId>.json to schema v2: an `active` map of per-role
pointers (token/tokenPool/lockBox/poolHooks) that HelperConfig resolves with
zero export, plus a `deployments` map of uniquely-named, versioned entries
(e.g. BnM-T_BurnMintTokenPool_2.0.0). The redeploy guard keys on the unique
deployments name, so a BurnMint and a LockRelease pool for one token — or an
old and a new pool version — coexist without collision. This is required by
the pool-migration flow, where the old and new pool must both be resolvable
across the setPool cutover. RegistryWriter.read resolves active.<role> with a
legacy fallback to the pre-v2 flat top-level key, and never reverts on a
concurrently-written (empty/partial) file. After each pool deploy, the script
asserts the on-chain typeAndVersion matches the version composed into the key.

Wire the previously dead registry read path: HelperConfig now resolves lockBox
and poolHooks through the same inline > {CHAIN}_ env > registry ladder as
token/tokenPool (getDeployedLockBox / getDeployedPoolHooks), so all four
artifacts resolve with no manual export. The allowlist, authorized-callers,
pool-deploy, and lockbox-operation scripts fall back to the getters; the
lockbox operations still revert with a clear message when nothing resolves.

Add a doctor rung reconciling the registry's active.tokenPool against the pool
wired in the on-chain TokenAdminRegistry: active is what this repo last
deployed, the TAR is what CCIP routes through, and they legitimately diverge
mid-migration — so divergence is reported as a WARN, never a FAIL.

The addresses/ registry is gitignored and local to the deploy machine; its
integrity comes from the single-writer recorder, not from git history (unlike
the git-tracked config/chains/*.json). Docs and the schema example are updated
to schema v2, the per-artifact keying table, the one-writer model, and the
active-vs-wired distinction.
@aelmanaa aelmanaa force-pushed the refactor/chain-config-registry-api-sync branch from 521ecda to 133f056 Compare July 9, 2026 23:32
@aelmanaa aelmanaa requested a review from SyedAsadKazmi July 9, 2026 23:48
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.

2 participants