Skip to content

serviceability: fix topology clear passing the topology PDA readonly - #4091

Closed
ben-dz wants to merge 1 commit into
mainfrom
bdz/topology-clear-writable-meta
Closed

serviceability: fix topology clear passing the topology PDA readonly#4091
ben-dz wants to merge 1 commit into
mainfrom
bdz/topology-clear-writable-meta

Conversation

@ben-dz

@ben-dz ben-dz commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Summary of Changes

  • Fixes doublezero link topology clear, which could never clear a topology that a link actually referenced. The SDK builder marked the topology PDA readonly while process_topology_clear writes it to decrement reference_count, so every invocation in which a link held the reference aborted with Topology Account is not writable — after the link writes had already been staged. Only two no-op cases succeeded: an already-closed topology, or no supplied link holding a reference. Broken since the CLI shipped in smartcontract,client: add topology CLI commands #3512.
  • Both the SDK builder and the program integration tests now build their account list from one clear_topology_account_metas function, so a client/processor writability divergence fails a test instead of a live transaction. Previously the SDK unit test asserted the buggy readonly meta as expected while the program test hand-built a writable one, and nothing compared them.
  • The processor validates the topology account up front via validate_program_account! rather than asserting writability part-way through the link loop. A readonly non-empty topology account is now rejected deterministically instead of only when at least one link held a reference.
  • Adds coverage for the multi-link reference_count decrement, which was unreachable before this fix — including the duplicate-link case, whose safety rests on the runtime aliasing duplicate accounts onto one data buffer.
  • A clear against an already-closed topology now logs that it was a stale-reference cleanup, instead of emitting the same line as a live clear (a typo'd --name previously read as success).

Related: RFC-18. The rest of the RFC-18 flex-algo surface was audited for the same client/processor divergence — topology create/delete/assign, link create/update/delete, device interface create/update, tenant update — and clear was the only one affected.

Not in scope: #4090, an unrelated reference_count skew via duplicate link_topologies entries in link update/link delete, surfaced while reviewing this change.

Behavior change

An operator on a pre-fix CLI binary now gets a hard failure in the cleared_count == 0 case where they previously got a silent success. No wire or account-layout change, and the stricter check only rejects input that no working client sends.

Diff Breakdown

Category Files Lines (+/-) Net
Tests 1 +308 / -34 +274
Core logic 2 +71 / -39 +32
Docs 1 +1 / -0 +1
Total 4 +380 / -73 +307

Overwhelmingly test weight — the fix is ~10 lines of logic plus a shared account-meta builder; the rest is closing the coverage gap that let the bug ship.

Key files (click to expand)
  • smartcontract/programs/doublezero-serviceability/tests/topology_test.rs — the clear helpers now build from clear_topology_account_metas; adds test_topology_clear_readonly_topology_rejected (both the no-reference case, which the pre-fix processor accepted, and the staged-write case) and test_topology_clear_multi_link_reference_count
  • smartcontract/programs/doublezero-serviceability/src/processors/topology/clear.rs — adds the canonical account-meta builder with the topology PDA writable; moves owner/writable validation ahead of the link loop; splits the success log for the already-closed path
  • smartcontract/sdk/rs/src/commands/topology/clear.rs — builds from the shared function instead of a hand-written list, and derives the globalstate PDA rather than threading it in

Testing Verification

  • Confirmed the anti-drift guard actually bites: reverting clear_topology_account_metas to new_readonly fails four existing tests (clear_removes_from_links, clear_is_idempotent, clear_with_permission_account_allowed, reference_count_lifecycle). The same divergence was invisible to every test before this change.
  • Confirmed the new regression test discriminates: reverting the processor to the pre-fix ordering (writability asserted after the link loop, guarded by cleared_count > 0) fails it with expected ProgramFailedToComplete (readonly topology account, no link holding a reference), got Ok(()) — the pre-fix code silently succeeded on that input. A test using only the reference-holding case passes against the bug, because Solana rolls the staged link write back either way.
  • test_topology_clear_multi_link_reference_count exercises two tagged links plus an untagged one plus a duplicate in a single instruction, asserting the count drops by 2 rather than 3, then deletes the topology to confirm the ReferenceCountNotZero guard it gates.
  • 52 test targets green across doublezero-serviceability, doublezero_sdk, and doublezero-serviceability-cli.

The SDK builder marked the topology account readonly while process_topology_clear
writes it to decrement reference_count, so every invocation in which a link
actually held the reference aborted with 'Topology Account is not writable' after
the link writes had already been staged. Only the two no-op cases succeeded:
an already-closed topology, or no supplied link holding a reference.

The account list now comes from clear_topology_account_metas, consumed by both
the SDK builder and the program integration tests, so a client/processor
writability divergence fails a test rather than a live transaction. The
processor validates the topology account up front via validate_program_account!
instead of asserting writability part-way through the link loop, so a readonly
non-empty topology is rejected deterministically rather than only when
cleared_count > 0.

Also covers the multi-link reference_count decrement, which was unreachable
before this fix, including the duplicate-link case.
@ben-dz
ben-dz force-pushed the bdz/topology-clear-writable-meta branch from a5999a5 to e7dc37a Compare July 27, 2026 20:30
@ben-dz

ben-dz commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Superseded. The readonly-topology bug this PR set out to fix was independently fixed in #4060 (RFC-26 R10), tracked as #4078: the clear_topology builder in doublezero-serviceability-instruction passes the topology PDA writable, and ClearTopologyCommand now delegates to it. R10 also added rfc26_builders_test.rs, which runs the builders against the real program via a dev-dependency cycle — the same anti-drift guard this PR was building by hand.

What remained after rebasing onto that was not worth a separate review cycle: the processor's up-front account validation guards against a caller that no longer exists now that the builder is the only path constructing this instruction, and it would tighten behavior on a live program for no current benefit.

One piece of coverage is worth salvaging and will land separately: saturating_sub(cleared_count) with cleared_count > 1 has still never executed anywhere — not in R10's single-link test, and not on any cluster, since the bug aborted every invocation that would have reached it — and it is the value the TopologyDelete guard depends on. The duplicate-link case matters more: its safety depends on the runtime aliasing duplicate accounts onto one data buffer, which is currently undocumented and untested.

That follow-up is blocked on a pre-existing flake in test_topology_reference_count_lifecycle, which fails roughly 1 in 4 runs on clean main under load (Custom(13) ReferenceCountNotZero on a delete immediately after an assertion read reference_count == 0). Any test added to that file raises its hit rate, so the flake is being fixed first.

@ben-dz ben-dz closed this Jul 27, 2026
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.

1 participant