Skip to content

sdk,cli: send the per-pass-type access pass removal instructions - #4268

Open
bgm-malbeclabs wants to merge 10 commits into
access-pass-programfrom
access-pass-callers
Open

sdk,cli: send the per-pass-type access pass removal instructions#4268
bgm-malbeclabs wants to merge 10 commits into
access-pass-programfrom
access-pass-callers

Conversation

@bgm-malbeclabs

@bgm-malbeclabs bgm-malbeclabs commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Part of malbeclabs/infra#2470. Child of malbeclabs/infra#2385.

Stacked on #4267. Merge that one first — this branch targets it, not main.

#4267 added one removal instruction per AccessPassType without changing anything for existing callers. This PR flips the switch: it deprecates CloseAccessPass (variant 69) and DeleteUser (variant 42), and moves every caller in the repository onto the new instructions — the builders, both SDKs, the CLI, the client daemon, the stress tools and the e2e suite.

The deprecation and the migration have to land together. CI proved it: splitting them left a branch whose own CLI could not delete a user.

Summary of Changes

  • doublezero access-pass close requires a new --type flag and doublezero user delete requires --access-pass-type, both with values prepaid, solana-validator, solana-rpc, others, edge-seat. Neither has a default, deliberately. A default would pick a kind for an operator who did not state one, which is the exact failure this change exists to prevent.
  • close_access_pass and delete_user in the instruction crate now pick their variant from an AccessPassKind the caller supplies.
  • CloseAccessPassCommand and DeleteUserCommand carry a required kind field.
  • The Go SDK's Executor.DeleteUser takes a kind. Its zero value is AccessPassKindUnspecified and is rejected, rather than silently meaning Prepaid — Go cannot make a parameter required, so the zero value has to fail loudly.
  • CliAccessPassType moves out of accesspass/set.rs into accesspass/types.rs so user delete can share it. No second enum was introduced.
  • CHANGELOG gains a Breaking entry, and the user delete table in smartcontract/cli/README.md gains the new flag.

Where the declared kind comes from, and where it does not

The program's refusal only bites when the kind carries the caller's intent. A caller that reads the pass and picks the instruction from what it read has chosen the instruction from the very byte the program then checks, so the two always agree.

The guard is live on the two paths the issue is about: both CLI verbs, where an operator types the kind and nothing in between re-reads the pass.

Three callers deliberately read the kind back, because no operator exists to declare one. Each carries a comment saying so, and saying that the refusal cannot fire there:

  • doublezero disconnect — a self delete; the handler already checks the owner and the client IP.
  • DeleteTenantCommand and its CLI equivalent — a cascade that sweeps users across mixed kinds. Forcing one declared kind would turn "delete every user under this tenant" into "delete only users of one kind", which strands the tenant record while it waits for reference_count to reach 0.

That list is meant to stay at three. A fourth read-back is how the feature quietly becomes decoration.

Breaking

Old CLI binaries break, not only the oracle. A user on a pre-0.39.0 doublezero disconnect sends variant 42 and gets Deprecated until they upgrade. e2e/compatibility_test.go records this as four knownIncompatibilities entries with before("0.39.0")if the release lands as something other than 0.39.0 those entries silently stop matching and need one edit.

One error-shape change worth knowing: the kind check runs before the existing authorisation checks, so a caller that used to get NotAllowed or Unauthorized may now get AccessPassTypeMismatch for the same transaction. That ordering is intentional, so a wrong-kind call fails for the reason it actually failed. Nothing in this repository branches on those codes.

Diff Breakdown

Category Files Lines (+/-) Net
Code 19 +787 / -93 +694
Tests 6 +71 / -22 +49
Docs 2 +10 / -3 +7
Total 27 +868 / -118 +750

The Code row overstates the production change: most of the growth in the Rust files is inline #[cfg(test)] modules, which the categoriser cannot split out. disconnect.rs (+149) and sdk/rs/commands/user/delete.rs (+139) are the two largest, and both are mostly new tests.

Key files (click to expand)
  • crates/doublezero-daemon-cli/src/disconnect.rs — derives the kind from the pass via the trait's existing get_accesspass, plus a test that seeds a SolanaValidator pass and asserts that exact kind reaches delete_user.
  • smartcontract/sdk/rs/src/commands/user/delete.rs — the kind field, and a second test using a distinct kind so a hardcoded value could not pass.
  • crates/doublezero-serviceability-instruction/src/user.rs — variant selection, and the restored multicast_publisher_count write-back.
  • smartcontract/sdk/go/serviceability/executor.go — the Go AccessPassKind, the selector, and the rejected zero value.
  • smartcontract/cli/src/accesspass/close.rs and src/user/delete.rs — the two required flags.
  • smartcontract/cli/src/accesspass/types.rsCliAccessPassType and its conversion, moved here so both verbs share one enum.
  • e2e/compatibility_test.go — the four knownIncompatibilities entries, plus the flag on every invocation.

Testing Verification

  • Both new CLI flags are proved required by a clap try_parse_from test: omitting the flag is an error, not a silent default.
  • Every command test uses two distinct kinds. With a single kind per file, an implementation that ignored its kind field and hardcoded that literal would have passed.
  • The daemon test seeds a non-Prepaid pass and asserts the matching kind reaches delete_user. Without it, a derivation that always returned Prepaid would break disconnect for every non-prepaid user with no test noticing.
  • The builders' byte selection is pinned per kind in three independent places — the Rust instruction crate, and the Go SDK — so a transposed pair fails rather than silently routing a removal to the wrong instruction.
  • All eleven e2e invocations were traced to the access-pass set --accesspass-type call in their own test before choosing a value.
  • make rust-lint clean; cd smartcontract && make test-programs 78 binaries green; rust-program-accounts-compat pass; Go serviceability and tools/stress green; go vet -tags e2e and go build -tags e2e clean.
  • TestE2E_BackwardCompatibility needed cascadeKnownFail: true on user_delete. An older CLI cannot send the required flag, and every later delete phase depends on that user being gone, so without cascading each dependent step failed on its own for a reason already recorded. CI caught this on the first run.
  • The e2e suite was not run locally — no Docker daemon was available on the machine this was built on. The changed e2e lines are string edits to CLI invocations and every behavioural claim above is covered by program integration tests that did run, but the four knownIncompatibilities entries can only be exercised against real older CLI images in CI.

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