Skip to content

WIP: [SEPARATION] SubscribeNodeGroupStatus: per-node-group chain status - #316

Open
l0gun0v wants to merge 1 commit into
mainfrom
separation-nodecore
Open

WIP: [SEPARATION] SubscribeNodeGroupStatus: per-node-group chain status#316
l0gun0v wants to merge 1 commit into
mainfrom
separation-nodecore

Conversation

@l0gun0v

@l0gun0v l0gun0v commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Design: see the design doc (Notion) — the problem, the group key and hash, the full stream contract, risks and the phase-2 plan live there. This description covers what is in the diff and how to review it.

What this adds

A dedicated RPC that streams chain status per node group — the upstreams
of a chain sharing a client_type label and an identical supported call-method
set:

rpc SubscribeNodeGroupStatus (SubscribeNodeGroupStatusRequest)
    returns (stream SubscribeChainStatusResponse) {}

SubscribeChainStatus is untouched and keeps carrying only the merged view;
the new stream carries only groups. The two never mix in one connection
(proto: drpcorg/emerald-grpc#166).

Contract in short: a head-gated full response per live group on subscribe
(each full carries BuildInfo — the consumer's min-version check reads it),
then group-scoped deltas, with a snapshot per announced group on the periodic
resync. A group-tagged ChainStatus: UNAVAILABLE is the removal signal; an
unavailable group is never announced, a recovered or re-formed one is
re-announced with a fresh full, and the resync tombstones announced groups
that vanished (so a lost removal delta is repaired within one interval).

Grouping itself: key is (client_type, methods_hash) where methods_hash is
a truncated sha256 over the sorted method set; membership is dynamic (label
re-detection and method bans move an upstream between groups); network logic
stays authoritative — lag validation keeps measuring upstreams against the
network head, per-group heads are observability only.

Notes for reviewers

  • Group events travel on a dedicated SubscriptionManager, not the shared
    one: SubscribeState feeds three production consumers (the local
    sub-engines in subengine/heads.go, blockupdates.go,
    pending_tx_source.go), and per-group per-block head wrappers have no
    business in their hot channels.
  • Group tracking is always on; only the stream is opt-in. Group count per
    chain is tiny, and lazy activation would need fork-choice reconstruction at
    first subscribe.
  • The group key is memoized per upstream (groupMembership): state events
    fire on every finalized/safe/lower-bound advance, while the hash input only
    changes on ban/unban (methods are copy-on-write — pointer identity is a
    valid change detector) or a client_type flip.
  • groups is keyed by the wire id (GroupKey.Id() is injective), so the
    stream's per-delta lookup is a plain map load.
  • One byte-visible change on the legacy stream: a caps-only delta used to
    go out as a ChainDescription with a single empty ChainEvent
    (pre-existing artifact of the unmapped CapsWrapper); such frames are now
    suppressed. Isolated in stateWrappersToResponse if literal
    bit-compatibility is preferred.

Files

File What changed
emerald-grpc (submodule) pinned to drpcorg/emerald-grpc#166
pkg/dshackle/*.pb.go regenerated: new RPC stubs, SubscribeNodeGroupStatusRequest, ChainDescription.NodeGroupId
internal/upstreams/node_groups.go new — group logic: GroupKey/methodsHash/clientTypeOf, memoized groupMembership, groupState (own HeightForkChoice + atomic state), assignGroup/leaveGroup/recomputeGroup/updateGroupHead, accessors (SubscribeNodeGroupStates, GetNodeGroupState(s))
internal/upstreams/chain_supervisor.go group fields + dispatch hooks on the four event types in processEvents; updateState moved onto the shared recomputeState; initial-state literal extracted
internal/upstreams/chain_supervisor_state.go merge-pipeline extraction (recomputeState, minStatus, processSubMethods as a free function) shared by the network and group paths — behavior-preserving
internal/upstreams/state_wrappers.go NodeGroupId on ChainSupervisorStateWrapperEvent (always empty on the network manager)
internal/upstreams/interfaces.go ChainSupervisor grew the three group accessors
internal/server/emerald/sub_node_group_status.go new — the group stream: announceGroup/syncGroups (single announce point: head- and status-gated fulls, snapshots, tombstones, groupFullSent bookkeeping)
internal/server/emerald/sub_chain_status.go streaming scaffold (streamChainStatuses) now shared by both RPCs; merged path otherwise as before; caps-only suppression; one full-response builder
internal/server/emerald/grpc_blockchain.go SubscribeNodeGroupStatus handler
internal/upstreams/node_groups_test.go key derivation: golden hash format, order-independence, unknown fallback, caps/SubMethods excluded
internal/upstreams/chain_supervisor_groups_test.go lifecycle on a real supervisor: create/merge (identical nodes aggregate, quorum=2), 4 upstreams → 3 groups (twins share one; same method set under two client types = same hash suffix, different groups; a twin leaving keeps the group), moves on methods/client_type change, drop on removal, per-group vs network fork choice, whole-group downgrade, network-stream isolation
internal/server/emerald/sub_node_group_status_test.go stream contract scenario by scenario, plus an end-to-end test driving a real supervisor through the real stream loop
remaining *_test.go, pkg/test_utils/test_helpers.go stubs for the new interface methods, shared event builder, signature updates
docs/nodecore/12-grpc-server.md the new RPC and its contract

Suggested review order

  1. node_groups.go top-to-bottom (identity → membership → recompute/heads).
    Concurrency model: groups/upstreamGroup/members/fork choice are
    mutated only by the processEvents goroutine (same ownership as
    lastOver); cross-goroutine readers touch only the CMap and the
    per-group Atomic.
  2. chain_supervisor.go — verify the network path is untouched apart from the
    extraction (the recomputeState body is the old updateState verbatim)
    and the four dispatch hooks.
  3. sub_node_group_status.go — the groupFullSent invariant is stated above
    the goroutine: the map mirrors what the consumer knows; anything that
    tells the consumer "unavailable" must clear it.
  4. sub_node_group_status_test.go as the executable contract.

Testing

  • go test -race -p 8 ./..., golangci-lint (CI config) clean, e2e tag
    compiles; group-stream tests run repeatedly under -race.
  • Live smoke against real endpoints: with identical and method-restricted
    upstreams on one instance, the group RPC announces exactly one full per real
    node class (identical nodes aggregate into one group with NodeDetails
    quorum 2) and carries no merged frame, while SubscribeChainStatus on the
    same instance keeps returning only the merged view; per-block tagged deltas
    and 1-min resync snapshots verified.
  • Cross-checked end-to-end against the consumer side (drpcorg/dproxy#2804).

Rollout

Land drpcorg/emerald-grpc#166 first, then move this branch's submodule pin to
the merged sha. Existing consumers are unaffected — they keep calling
SubscribeChainStatus. No config, no routing changes.

@l0gun0v l0gun0v changed the title DRPCPROT-603 WIP: [SEPARATION] provide aggregated height/lower bound for each node type Aug 2, 2026
@l0gun0v
l0gun0v force-pushed the separation-nodecore branch from c362356 to 8d9195d Compare August 2, 2026 21:14
@l0gun0v l0gun0v changed the title WIP: [SEPARATION] provide aggregated height/lower bound for each node type WIP: [SEPARATION] per-node-group chain status in SubscribeChainStatus Aug 2, 2026
@l0gun0v
l0gun0v force-pushed the separation-nodecore branch from 8d9195d to 55eddb5 Compare August 19, 2026 09:51
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@l0gun0v
l0gun0v force-pushed the separation-nodecore branch from 55eddb5 to 7d74b41 Compare August 19, 2026 09:51
@l0gun0v l0gun0v changed the title WIP: [SEPARATION] per-node-group chain status in SubscribeChainStatus WIP: [SEPARATION] SubscribeNodeGroupStatus: per-node-group chain status Aug 19, 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