Skip to content

fix: unread is a state, incomplete is a verdict, unset is not zero - #23

Merged
SyedAsadKazmi merged 1 commit into
mainfrom
fix/silent-substitution
Aug 11, 2026
Merged

fix: unread is a state, incomplete is a verdict, unset is not zero#23
SyedAsadKazmi merged 1 commit into
mainfrom
fix/silent-substitution

Conversation

@aelmanaa

Copy link
Copy Markdown
Contributor

This pull request updates documentation and configuration to clarify and enforce the rules for rate limiter inputs, doctor/roles-check verdicts, and dynamic config behaviors. The changes improve accuracy, make error handling more explicit, and ensure that configuration tools and documentation reflect the actual contract and script behavior, especially around partial inputs and environment variable handling.

Key changes include:

Rate Limiter Input Rules and Documentation

  • Clarified that rate limiter inputs (*_CAPACITY, *_RATE, *_ENABLED) are all-or-nothing per direction: both capacity and rate must be supplied together to enable a bucket, and partial inputs are refused with a clear error. This rule is now documented in multiple locations, including docs/operations/rate-limits.md, docs/primitives/_meta.json, and referenced in operation guides. [1] [2] [3] [4] [5] [6]

  • Updated the primitives catalog and meta documentation to include detailed input descriptions for all relevant operations (UpdateRateLimiters, SetFinalityConfig, etc.), specifying the all-or-nothing rule and the behavior for unset or disabled buckets. [1] [2] [3]

Doctor and Roles-Check Verdicts

  • Changed documentation and CI output to distinguish between VERIFIED, INCOMPLETE, and FAILED outcomes for make doctor and roles-check, clarifying that INCOMPLETE (e.g., due to missing RPC env) is not a pass and must be addressed before launch. Output and documentation now explain how refused reads and infrastructure gaps are handled. [1] [2] [3] [4] [5]

  • Improved explanation of how unreadable fields are handled in snapshots and audits, making it clear that missing reads are not written as zero/empty values and cause a FAIL rather than a PASS.

Dynamic Config and Environment Variable Handling

  • Updated documentation for dynamic config scripts (SetDynamicConfig, etc.) to clarify that unset environment variables preserve the current on-chain value verbatim, including address(0), and that no fallback (such as broadcaster) is applied unless explicitly set. [1] [2]

Additional Documentation Improvements

  • Improved accuracy and clarity in guides, including the mesh verification process and the meaning of a VERIFIED doctor run, and updated several examples and explanations for consistency with the above changes. [1] [2] [3]

These updates ensure that users and developers have clear, accurate guidance on configuration, error handling, and expected behaviors in both tooling and on-chain operations.

Three mechanisms turned a read that never happened into a clean report, and
each gets closed at its root rather than per symptom.

Probes report whether they read. The six tolerant probes in RolesProbes return
a zero value beside an ok flag, and most call sites dropped the flag, so a
contract the audit could not read became the assertion "the chain holds 0x0,
false, an empty set". A snapshot then wrote those zeros into the declaration
and the next audit compared them against the same failed reads: the two agreed
because neither observed anything, and `sets match` was printed over a contract
that answers no getter. Now the snapshot writes an unread field as ABSENT,
never as the probe's zero (each omission is logged as UNREAD), the audit
refuses to compare a declared value against a failed read (a FAIL naming the
getter, not a PASS), and a declared hooks or lockbox contract that answers none
of its getters fails once as unauditable instead of cascading. The write
scripts that gated authority on a probed owner or admin refuse a failed read
before the equality check, whose message would otherwise attribute an owner of
0x0 to a token whose owner was never observed. Old project files may carry
zeros written by failed reads; against a still-unreadable contract they now
fail instead of reconciling, and a re-run of snapshot-chain on a working RPC
rebuilds the block from actual reads (docs/config-schema.md documents the
absence-is-unread rule).

The doctor returns three outcomes, not a boolean. A run that skipped the RPC
rung, the TAR reconcile, the lanes rung and the roles rung printed 0 FAIL,
0 WARN and exited 0, indistinguishable from a run that verified everything.
check-chain now counts unverified gaps (a check that was declared or deployed
but could not run: an unset RPC, a pool that does not answer), tags each
[SKIP] UNVERIFIED so the verdict points at exactly those lines, and ends
VERIFIED, INCOMPLETE, or FAILED, with INCOMPLETE reverting under its own
marker so a wrapper gating on the exit code cannot read an unchecked chain as
a verified one. Designed absences (a non-EVM chain's EVM rungs, an undeclared
optional block) stay plain SKIPs and never taint the verdict, so a fully
verified run still exits 0 and prints VERIFIED. The roles-check script
contract (0/1/2) is unchanged: after the probe fix, every
declared-but-unverifiable field is a FAIL there, so its fails-only gate is
sound; docs/roles.md documents that exit 1 also covers refused reads and adds
the nothing-was-compared branch to the drift runbook. snapshot-chain counts
its UNREAD fields and warns that the written block is partial rather than
closing success-shaped.

Unset variables do not become on-chain values. The rate-limit readers defaulted
any unsupplied field of a triggered direction to 0 and wrote it: capacity alone
enabled a bucket with rate 0 (the lane works for N tokens, then is permanently
dead with TokenRateLimitReached), ENABLED=true alone wrote capacity 0 (every
transfer reverts TokenMaxCapacityExceeded), and both printed a success message.
A direction's inputs are now all-or-nothing through one shared decision, used
by all three consumers (UpdateRateLimiters, SetFinalityConfig, and
ApplyChainUpdates' CLI mode): an enabled bucket needs CAPACITY and RATE
supplied together, refusal names the missing variable, ENABLED=false alone
stays valid (a disabled bucket's zeros are forced by protocol rule, and a
nonzero alongside is refused here rather than left to revert on chain), and a
value that does not fit uint128 is refused rather than truncated: 1e39 wraps
to a plausible wrong capacity and 2^128 to exactly 0. SetDynamicConfig now
preserves an unset field's current on-chain value verbatim, address(0)
included: it writes the whole struct, and a broadcaster fallback would turn a
ROUTER-only run into a silent grant of both admin slots to the acting account.

The primitives catalog had its own instance of the defect: authored inputs
(for env vars read through test seams, invisible to the generator's vm.env*
scan) reached only the markdown pages, while catalog.json kept advertising
"inputs": [] for scripts that do read env vars. The generator now applies
authored inputs at the source, the same at-the-source rule it already applies
to the destructive flag, and the seam-read rate-limit and dynamic-config
variables are authored in _meta.json so both the pages and the machine index
name them.

The decision logic is pure and pinned without touching the process env
(vm.setEnv is process-wide while tests run in parallel, and the apply scripts
read the same variables): test/roles/UnreadableIsNotEmpty.t.sol reproduces the
silent reconcile and its snapshot half, test/config/VerifyChainVerdict.t.sol
pins the three outcomes and that a designed skip never taints them,
test/configure/RateLimitBucketInputs.t.sol pins the bucket shapes, and
test-tooling.sh asserts the rpcEnv-unset doctor run is INCOMPLETE and nonzero.
@aelmanaa
aelmanaa requested review from a team as code owners August 11, 2026 09:39
@github-actions

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 August 11, 2026 09:39
@SyedAsadKazmi
SyedAsadKazmi merged commit b762184 into main Aug 11, 2026
12 checks passed
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