Skip to content

feat(core): implement slotUpdatesSubscribe WS RPC method#677

Open
MicaiahReid wants to merge 1 commit into
mainfrom
feat/ws-slotUpdatesSubscribe
Open

feat(core): implement slotUpdatesSubscribe WS RPC method#677
MicaiahReid wants to merge 1 commit into
mainfrom
feat/ws-slotUpdatesSubscribe

Conversation

@MicaiahReid
Copy link
Copy Markdown
Collaborator

No description provided.

@MicaiahReid MicaiahReid requested a review from lgalabru May 27, 2026 13:56
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 27, 2026

Greptile Summary

This PR implements the slotsUpdatesSubscribe WebSocket RPC method, completing a previously stubbed-out subscription endpoint. It wires up the full slot lifecycle (Frozen, CreatedBank, OptimisticConfirmation, Root) from SVM internals through a fan-out channel to connected WS clients, with Arc<SlotUpdate> sharing to avoid redundant allocations across multiple subscribers.

  • SurfnetSvm gains a slots_updates_subscriptions vec and two methods (subscribe_for_slots_updates, notify_slots_updates_subscribers), mirroring the existing slot_subscriptions pattern; confirm_transactions is extended to return the failed-transaction count needed for SlotTransactionStats.
  • SurfpoolWsRpc::slots_updates_subscribe spawns a per-subscription async polling task; slots_updates_unsubscribe removes the sink from the shared map, which causes the task to self-terminate on its next 50 ms tick.
  • Five integration tests cover the basic stream, per-lifecycle variants, Root emission after FINALIZATION_SLOT_THRESHOLD advancements, multiple concurrent subscribers, and multi-slot sequences.

Confidence Score: 4/5

The implementation is functionally correct and well-tested; the findings are minor accuracy and logging issues that do not affect data integrity or correctness of the subscription lifecycle.

The unsubscribe polling loop logs a spurious error on every normal client disconnect, and SlotTransactionStats always reports a hardcoded num_transaction_entries of 1. Neither causes incorrect behavior for the subscription stream itself, but clients consuming the stats field will receive an inaccurate entry count and production logs will be noisy on routine unsubscribes. The Root event also carries the current slot's wall-clock timestamp rather than the timestamp from when that older slot was actually rooted, which may mislead time-sensitive consumers. All three issues are confined to the new code paths and do not touch existing subscription logic.

crates/core/src/rpc/ws.rs (polling loop log level) and crates/core/src/surfnet/svm.rs (stats accuracy and Root timestamp)

Important Files Changed

Filename Overview
crates/core/src/rpc/ws.rs Implements slots_updates_subscribe/unsubscribe WebSocket handlers; polling loop has a misleading error log on the normal unsubscribe exit path
crates/core/src/surfnet/svm.rs Adds slot-lifecycle fan-out (Frozen/CreatedBank/OptimisticConfirmation/Root); num_transaction_entries hardcoded to 1 and Root event reuses current-slot timestamp rather than the older rooted slot's wall-clock time
crates/core/src/surfnet/locker.rs Adds subscribe_for_slots_updates delegating to the SVM writer; mirrors the existing slot-update subscription pattern cleanly
crates/core/src/surfnet/mod.rs Adds SlotsUpdatesSubscriptionData type alias and Arc import; straightforward addition consistent with other subscription types
crates/core/src/runloops/mod.rs Initializes slots_updates_subscription_map in the WS RPC server struct; one-line addition consistent with other subscription map initializations
crates/core/src/tests/integration.rs Adds five integration tests covering basic streaming, manual advancement, Root emission, multiple subscribers, and multiple slot changes; good coverage of the new feature

Sequence Diagram

sequenceDiagram
    participant Client as WS Client
    participant WsRpc as SurfpoolWsRpc
    participant Map as slots_updates_subscription_map
    participant Task as Async Polling Task
    participant Locker as SurfnetSvmLocker
    participant SVM as SurfnetSvm

    Client->>WsRpc: slotsUpdatesSubscribe
    WsRpc->>WsRpc: assign_id → sink
    WsRpc->>WsRpc: get_svm_locker()
    WsRpc->>Task: spawn async task
    Task->>Map: insert(sub_id, sink)
    Task->>Locker: subscribe_for_slots_updates()
    Locker->>SVM: with_svm_writer → push tx
    SVM-->>Locker: rx
    Locker-->>Task: rx

    loop Every slot
        SVM->>SVM: notify_slots_updates_subscribers(Frozen)
        SVM->>SVM: notify_slots_updates_subscribers(CreatedBank)
        SVM->>SVM: notify_slots_updates_subscribers(OptimisticConfirmation)
        SVM-->>Task: "Arc<SlotUpdate> via channel"
        Task->>Map: read lock → get sink
        Task->>Client: sink.notify(Ok(slot_update))
    end

    Client->>WsRpc: slotsUpdatesUnsubscribe(sub_id)
    WsRpc->>Map: write lock → remove(sub_id)
    Task->>Map: read lock → get → None
    Task->>Task: break loop (task exits)
Loading

Comments Outside Diff (1)

  1. crates/core/src/surfnet/svm.rs, line 2539-2542 (link)

    P2 Root event carries the wrong wall-clock timestamp. slots_update_ts is sampled once at the start of confirm_current_block for the current slot. The Root notification, however, is for a slot FINALIZATION_SLOT_THRESHOLD (≈32) slots in the past — it should reflect when that older slot was rooted, not the current wall-clock time. Clients that record or display when a slot was rooted will see it stamped with the time the current slot closed, introducing an error of roughly 32 × slot_time (~13 seconds at 400 ms slots). Storing the timestamp alongside each slot as it is created/frozen and reusing it here when the slot is finally rooted would fix the semantic mismatch.

Reviews (1): Last reviewed commit: "feat(core): implement `slotUpdatesSubscr..." | Re-trigger Greptile

Comment thread crates/core/src/rpc/ws.rs
Comment on lines +1963 to 1976
let Some(sink) = guard.get(&sub_id) else {
log::error!("Failed to get sink for subscription ID");
break;
};

if let Err(e) = sink.notify(Ok(slot_update)) {
log::error!("Failed to notify client about slots update event: {e}");
break;
}
}
});
}

fn slots_updates_unsubscribe(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 False-positive error log on normal unsubscribe. Between the first map check (which finds the subscription present) and the second read lock acquisition after try_recv, slots_updates_unsubscribe can race in and remove the entry. When it does, guard.get(&sub_id) legitimately returns None — this is the expected clean-exit path, not an internal error. Logging at error level here will pollute production logs with a spurious error on every normal unsubscribe.

Comment on lines +2355 to +2364
self.notify_slots_updates_subscribers(SlotUpdate::Frozen {
slot,
timestamp: slots_update_ts,
stats: SlotTransactionStats {
num_transaction_entries: 1,
num_successful_transactions,
num_failed_transactions,
max_transactions_per_entry: num_transactions,
},
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 num_transaction_entries is always hardcoded to 1, and consequently max_transactions_per_entry always equals the total transaction count. On real Solana a block can contain multiple entries (parallel execution batches). While Surfpool's single-threaded execution model means all transactions end up in one logical entry today, hardcoding it means clients that rely on the spec-correct value will silently receive inaccurate data. At minimum a named constant would make the limitation explicit and easier to fix later.

Suggested change
self.notify_slots_updates_subscribers(SlotUpdate::Frozen {
slot,
timestamp: slots_update_ts,
stats: SlotTransactionStats {
num_transaction_entries: 1,
num_successful_transactions,
num_failed_transactions,
max_transactions_per_entry: num_transactions,
},
});
// Surfpool executes transactions sequentially in a single entry per block.
const SURFPOOL_ENTRIES_PER_BLOCK: u64 = 1;
self.notify_slots_updates_subscribers(SlotUpdate::Frozen {
slot,
timestamp: slots_update_ts,
stats: SlotTransactionStats {
num_transaction_entries: SURFPOOL_ENTRIES_PER_BLOCK,
num_successful_transactions,
num_failed_transactions,
max_transactions_per_entry: num_transactions,
},
});

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