Skip to content

feat(healthcheck): whitelist invariant reads the timelock queue — queued syncs report as expected-pending (EXSC-918) - #2309

Merged
0xDEnYO merged 3 commits into
mainfrom
healthcheck-whitelist-intent
Sep 2, 2026
Merged

0xDEnYO merged 3 commits into
mainfrom
healthcheck-whitelist-intent

Conversation

@0xDEnYO

@0xDEnYO 0xDEnYO commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Which Linear task belongs to this PR?

Fixes EXSC-918

Follow-up to EXSC-847 (#2270), which made facets-registered and periphery-registered intent-aware. The whitelist check was the one invariant of that family still reporting a rollout in flight as drift.

Why did I implement it this way?

config/whitelist.json is merged before the Safe proposal syncing it executes, so in between, checkWhitelistIntegrity errored on every pair the queued operation was about to add — on both of its steps (Source of Truth FAILED from isContractSelectorWhitelisted, and Pair Array is missing N pairs from config from getAllContractSelectorPairs). Live today: robinhood red on 0xCC89feed…8599 / 0xf8989325 while its sync operation sat queued; same shape on arc (09-01) and injective (08-27).

Two independent reasons #2270's machinery didn't already cover it, both addressed here:

  • The decoder ignored whitelist calls. extractRegistrations handled only diamondCut and registerPeripheryContract — a whitelist batch was explicitly documented as contributing nothing. It now also decodes batchSetContractSelectorWhitelist — the call diamondSyncWhitelist.sh and proposePeripheryWithWhitelist.ts actually emit — plus the singular setContractSelectorWhitelist defensively, both only with _whitelisted true; false un-whitelists and is ignored, the counterpart of a Remove cut. A length-mismatched batch reverts on-chain (InvalidConfig), so it decodes to nothing rather than to the pairs it could have paired up.
  • The downgrade was wired only into the two registration invariants. A new pure splitByPendingWhitelist splits missing pairs by whether a queued operation whitelists exactly that contract and selector on that network's diamond. Matching the address alone would report a sync that is never coming — the same reasoning that makes the periphery caller match the registry name.

Design points worth a reviewer's attention:

  • Records now carry an explicit kind (facet-cut / periphery / whitelist). facets-registered previously inferred "this is a cut" from peripheryName === undefined, which a whitelist record would have satisfied — a latent false-green on an error gate. Each consumer now matches its own shape, and there's a regression test that a queued whitelist entry does not downgrade a missing facet.
  • One lazy queue read, shared by both steps. Resolved only when a pair is actually missing, so a synced network never touches MongoDB (tested).
  • An unreachable queue keeps every error and adds a coverage warning, matching facets-registered: the pair comparison stands on an independent on-chain signal, and a Mongo blip turning a genuinely unsynced whitelist green is far worse than a false alert during a rollout.
  • Additions only. A stale pair (on-chain, no longer in config) still errors even while its removal is queued — the removal side reads parked tasks, which carry no whitelist payloads. Called out in [CONV:HEALTHCHECK-INTENT].
  • The two "is synced" success lines now name the pending count instead of claiming the diamond already holds what only a queued operation will put there.

Verified against the live queue (904 rows), not just fixtures. The single queued row is robinhood's sync — carrying exactly the red pair and targeting robinhood's own diamond (0xB477751B…14Af3) — so this turns today's alert into expected-pending. Every inner-call selector present across all 904 rows was resolved: batchSetContractSelectorWhitelist appears 266 times (408 whitelist records, of which 91 are _whitelisted=false and correctly decode to nothing), and none of the undecoded selectors writes whitelist storage. The legacy writers that would have been a genuine coverage gap (batchAddDex, batchSetFunctionApprovalBySignature, setContractWhitelist) appear zero times and have no emitter left in the repo. 74 distinct networks have carried a whitelist row historically, so the behaviour change is fleet-wide, not robinhood-specific.

Mutation-tested each new guard rather than trusting the suite: dropping kind === 'whitelist', dropping kind === 'periphery', widening the selector comparison, or removing the "skip the queue when nothing is missing" short-circuit each fail exactly one dedicated test. The first two initially passed — the fixtures standing in for "a record of another kind" did not carry the field the surviving predicate compares, so the comparison did the rejecting and the discriminator was never exercised. Fixed in the second commit.

Checklist before requesting a review

Checklist for reviewer (DO NOT DEPLOY and contracts BEFORE CHECKING THIS!!!)

  • I have checked that any arbitrary calls to external contracts are validated and or restricted
  • I have checked that any privileged calls (i.e. storage modifications) are validated and or restricted
  • I have ensured that any new contracts have had AT A MINIMUM 1 preliminary audit conducted on by <company/auditor>

…ued syncs report as expected-pending (EXSC-918)

`config/whitelist.json` is merged before the Safe proposal syncing it executes, so
between those events the config-vs-chain comparison reported a hard error on every
pair the operation was about to add. The registration invariants resolved that window
in EXSC-847; the whitelist check was the one left reporting a rollout in flight as
drift.

Both whitelist steps now grade missing pairs against the timelock execution queue and
downgrade a pair to expected-pending only when a queued operation whitelists exactly
that contract and selector on that network's own diamond. Decoded records carry an
explicit `kind`, so each consumer matches its own shape rather than the absence of a
field.
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 00d98c99-4dc7-4a26-8dd9-0b93426295fa

📥 Commits

Reviewing files that changed from the base of the PR and between 8f2be94 and b339ebc.

📒 Files selected for processing (5)
  • .agents/rules/601-healthcheck-invariants.md
  • script/deploy/healthCheckInvariants.test.ts
  • script/deploy/healthCheckInvariants.ts
  • script/deploy/safe/pending-registrations.test.ts
  • script/deploy/safe/pending-registrations.ts

Included review availability: Your plan provides up to 2 included reviews per hour; 0 remain after this review.


Walkthrough

The change adds typed pending whitelist registrations, decodes single and batch whitelist operations, and updates whitelist health checks to classify queued additions separately from uncovered pairs. Tests cover selector matching, queue failures, and registration-kind filtering.

Changes

Whitelist health-check coverage

Layer / File(s) Summary
Pending registration decoding
script/deploy/safe/pending-registrations.ts, script/deploy/safe/pending-registrations.test.ts
Pending records now include explicit kind values. Enabled single and batch whitelist calls produce contract-selector records. Revocations and invalid batches produce no records.
Whitelist integrity classification
script/deploy/healthCheckInvariants.ts, .agents/rules/601-healthcheck-invariants.md
Whitelist checks match missing pairs against queued whitelist records by contract and selector. Expected-pending additions log informational output, while uncovered pairs remain errors. Facet and periphery checks use explicit registration kinds.
Health-check validation coverage
script/deploy/healthCheckInvariants.test.ts
Tests cover exact and case-insensitive selector matching, ignored registration kinds, unreachable queues, memoized resolution, synced networks, and whitelist entries that do not satisfy facet-cut evidence.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🔵 Low · up to b339e

Missing whitelist pairs can now be reported as expected-pending when an exact queued sync exists, reducing false alerts during rollouts. The change is mergeable with owner awareness because stale, cancelled, or incorrectly maintained queue records could temporarily delay detection of real whitelist drift.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 4 files. (1 skipped: 1 u…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: the healthcheck whitelist invariant now reads the timelock queue and reports queued whitelist syncs as expected-pending. It is specific and concise enough…
Description check ✅ Passed The description follows the required template. It identifies EXSC-918, explains the implementation and design, records testing and validation, and completes the contributor checklist. The reviewer che…
Full details: Docstring Coverage

Explanation

Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 4 files. (1 skipped: 1 unsupported.)

Full details: Title check

Explanation

The title clearly identifies the main change: the healthcheck whitelist invariant now reads the timelock queue and reports queued whitelist syncs as expected-pending. It is specific and concise enough for repository history.

Full details: Description check

Explanation

The description follows the required template. It identifies EXSC-918, explains the implementation and design, records testing and validation, and completes the contributor checklist. The reviewer checklist remains appropriately unchecked for reviewer action.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch healthcheck-whitelist-intent

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

… read path (EXSC-918)

Both `kind` guards were inert: the fixtures standing in for "a record of another kind"
never carried the field the surviving predicate compares, so the comparison did the
rejecting and dropping the guard kept every test green. Each fixture now carries that
field, so only the discriminator separates them. Adds the multicall path — the one
production networks take — the repeated-contract batch shape the live queue actually
carries, and the two preconditions `splitByPendingWhitelist` does not check itself.
@0xDEnYO

0xDEnYO commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Review-gate pass (local trial) — nothing escalated, no critical findings. Recording what was not fixed, so a reviewer does not have to re-derive it:

  • STALE_QUEUE_GRACE_MS headroom is thinnest on exactly this consumer. Across 901 executed queue rows the create→execute spread is p50 3.3 h / max 70.7 h, and that fleet-wide max is a whitelist row (facet-cut rows top out at 20.2 h). The window is delay + 72 h ≈ 75 h, so ~4.3 h of margin; 0 rows have ever exceeded it. Left alone — widening it would weaken the never-landed-cut gate the bound exists for, and the failure direction is over-alerting.
  • blocked rows report as findings. listPendingRegistrationsByNetwork reads status: 'queued' only, so a whitelist sync sitting blocked still errors even though it becomes executable once an operator clears the cause. Nothing bounds how long that takes, so honouring it would be unbounded masking. Pre-existing from feat(healthcheck): intent-aware facets-registered + periphery-registered — scheduled registrations report as expected-pending (EXSC-847) #2270; this PR adds a third consumer to it, and the docstring now names the case instead of implying the status list is exhaustive.
  • The "that network's diamond" filter has no whitelist-path test. It lives in resolvePendingRegistrations (record.target === diamond), is locked by facets-registered's own wrong-target test, and is verified on real data here — but splitByPendingWhitelist does not re-check it. Documented as a caller-owned precondition rather than duplicated.
  • A coverage warning is lost if step 2 throws after step 1 recorded an unreachable queue. Cosmetic: every error is still reported, and the throw itself is logged.

Also worth knowing, since it changes when this code touches MongoDB: the queue read now fires on any missing whitelist pair, which is more common fleet-wide than a missing facet. During a Mongo outage the "queue unreachable" warning will therefore appear on materially more networks than #2270's did. Errors are preserved in that state.

@0xDEnYO
0xDEnYO marked this pull request as ready for review September 2, 2026 08:44
@0xDEnYO
0xDEnYO requested a review from a team September 2, 2026 08:44
@0xDEnYO

0xDEnYO commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@0xDEnYO

0xDEnYO commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 32 minutes.

@0xDEnYO
0xDEnYO enabled auto-merge (squash) September 2, 2026 09:02
@0xDEnYO
0xDEnYO merged commit 58b5ebe into main Sep 2, 2026
70 of 71 checks passed
@0xDEnYO
0xDEnYO deleted the healthcheck-whitelist-intent branch September 2, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants