smartcontract: fix multicast publisher/subscriber counter decrement on disconnect#3342
Conversation
smartcontract/programs/doublezero-serviceability/tests/user_onchain_allocation_test.rs
Show resolved
Hide resolved
|
Now that I have a better understanding of this, I think it's right. However, and this is not a blocker (that's why I'm approving), I think calling the new field on the User "multicast_publisher" is extremely confusing. I would probably make it a u8 value called accounting_flags. Then create an enum of AccountingFlags, that has a flag called CreatedAsPublisher. Ultimately, this flag is only used for tunnel accounting on the device. |
Spoke with @elitegreg and we decided to go with his approach. |
Add a boolean `multicast_publisher` field at the end of the `User` struct to record whether a user was activated as a multicast publisher. This enables correct counter decrements at delete time (the publishers list is always empty by then due to ReferenceCountNotZero). The field uses `unwrap_or_default()` in `TryFrom<&[u8]>` so existing onchain accounts without this byte deserialize to `false`, preserving backward compatibility. Also updates the `AIRDROP_USER_RENT_LAMPORTS_BYTES` constant (+1 byte per User account) and fixes the associated size assertions. Two new unit tests verify old-account backward compat and roundtrip.
Record user.multicast_publisher = (user_type == Multicast && !publishers.is_empty()) immediately before try_activate so that delete/closeaccount can decrement the correct device counter even after the publishers list has been cleared. Add two integration tests verifying the flag is true for an activated publisher and false for a user with no publisher subscriptions.
The original fix set multicast_publisher in activate.rs using unconditional assignment, which would reset the flag to false when the activator re-activated a user after disconnect (publishers list is empty at that point). Two root causes fixed: 1. create_user_core: set multicast_publisher=is_publisher at creation time, so the flag is established alongside the counter increment. 2. activate.rs: only set multicast_publisher on first activation (Pending status). Re-activation (Updating status) must never reset the flag — it must persist through the disconnect cycle so delete/closeaccount decrement the correct counter. Also add proper regression tests in create_subscribe_user_test.rs that exercise the full production path: CreateSubscribeUser(publisher=true) → activate → disconnect (unsubscribe + re-activate with empty publishers) → delete. These tests verify publishers_count is decremented from 1→0 (not subscribers_count).
6c65856 to
87fb6a4
Compare
Resolves: #3331
Summary of Changes
multicast_publishers_countnever decremented andmulticast_subscribers_countover-decremented when multicast users disconnected. Every departing user — publisher or subscriber —was decrementing
multicast_subscribers_count, because the counter logic checked!user.publishers.is_empty(), which is always false at delete time (theReferenceCountNotZeroguard requirespublishers to be empty before deletion is allowed).
tunnel_flags: u8field to theUserstruct with aCreatedAsPublisherbit, set at activation time when the user has a non-empty publishers list. The delete and closeaccountinstructions now use this flag instead of the broken runtime check.
Diff Breakdown
Mostly tests; the fix itself is 4 lines changed across 2 files.
Key files (click to expand)
smartcontract/programs/doublezero-serviceability/tests/user_onchain_allocation_test.rs— integration tests for activate, delete (atomic), and closeaccount (legacy) counter pathse2e/multicast_counter_lifecycle_test.go— E2E regression test: connect publisher + subscriber, disconnect each, assert correct counter values at every stepsmartcontract/programs/doublezero-serviceability/src/state/user.rs— addstunnel_flags: u8field withCreatedAsPublisherbit, backward-compatible deserialization (unwrap_or_default()), and unittests for old-account compat
smartcontract/programs/doublezero-serviceability/src/processors/user/activate.rs— setsCreatedAsPublisherflag when activating a user with a non-empty publishers listsmartcontract/programs/doublezero-serviceability/src/processors/user/delete.rs— replaces!user.publishers.is_empty()withTunnelFlags::is_set(user.tunnel_flags, TunnelFlags::CreatedAsPublisher)in atomic delete path
smartcontract/programs/doublezero-serviceability/src/processors/user/closeaccount.rs— same fix for legacy closeaccount pathTesting Verification
tunnel_flags = 0activate.rssets the flag correctly for publishers and subscribers; (2) atomic delete decrementsmulticast_publishers_countfor publishers and leavesmulticast_subscribers_countunchanged; (3) legacy closeaccount path does the sameTestE2E_Multicast_CounterLifecycle) verifies the full stack: publisher disconnect leavessubscribers_countintact; subscriber disconnect brings both counters to zeromake rust-lintandcargo testpass;cargo test-sbfis broken onorigin/main(lockfile v4 issue, pre-existing and unrelated)