WIP: [SEPARATION] SubscribeNodeGroupStatus: per-node-group chain status - #316
Open
l0gun0v wants to merge 1 commit into
Open
WIP: [SEPARATION] SubscribeNodeGroupStatus: per-node-group chain status#316l0gun0v wants to merge 1 commit into
l0gun0v wants to merge 1 commit into
Conversation
l0gun0v
force-pushed
the
separation-nodecore
branch
from
August 2, 2026 21:14
c362356 to
8d9195d
Compare
l0gun0v
force-pushed
the
separation-nodecore
branch
from
August 19, 2026 09:51
8d9195d to
55eddb5
Compare
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
l0gun0v
force-pushed
the
separation-nodecore
branch
from
August 19, 2026 09:51
55eddb5 to
7d74b41
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
A dedicated RPC that streams chain status per node group — the upstreams
of a chain sharing a
client_typelabel and an identical supported call-methodset:
rpc SubscribeNodeGroupStatus (SubscribeNodeGroupStatusRequest) returns (stream SubscribeChainStatusResponse) {}SubscribeChainStatusis 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: UNAVAILABLEis the removal signal; anunavailable 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)wheremethods_hashisa 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
SubscriptionManager, not the sharedone:
SubscribeStatefeeds three production consumers (the localsub-engines in
subengine/heads.go,blockupdates.go,pending_tx_source.go), and per-group per-block head wrappers have nobusiness in their hot channels.
chain is tiny, and lazy activation would need fork-choice reconstruction at
first subscribe.
groupMembership): state eventsfire 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_typeflip.groupsis keyed by the wire id (GroupKey.Id()is injective), so thestream's per-delta lookup is a plain map load.
go out as a
ChainDescriptionwith a single emptyChainEvent(pre-existing artifact of the unmapped
CapsWrapper); such frames are nowsuppressed. Isolated in
stateWrappersToResponseif literalbit-compatibility is preferred.
Files
emerald-grpc(submodule)pkg/dshackle/*.pb.goSubscribeNodeGroupStatusRequest,ChainDescription.NodeGroupIdinternal/upstreams/node_groups.goGroupKey/methodsHash/clientTypeOf, memoizedgroupMembership,groupState(ownHeightForkChoice+ atomic state),assignGroup/leaveGroup/recomputeGroup/updateGroupHead, accessors (SubscribeNodeGroupStates,GetNodeGroupState(s))internal/upstreams/chain_supervisor.goprocessEvents;updateStatemoved onto the sharedrecomputeState; initial-state literal extractedinternal/upstreams/chain_supervisor_state.gorecomputeState,minStatus,processSubMethodsas a free function) shared by the network and group paths — behavior-preservinginternal/upstreams/state_wrappers.goNodeGroupIdonChainSupervisorStateWrapperEvent(always empty on the network manager)internal/upstreams/interfaces.goChainSupervisorgrew the three group accessorsinternal/server/emerald/sub_node_group_status.goannounceGroup/syncGroups(single announce point: head- and status-gated fulls, snapshots, tombstones,groupFullSentbookkeeping)internal/server/emerald/sub_chain_status.gostreamChainStatuses) now shared by both RPCs; merged path otherwise as before; caps-only suppression; one full-response builderinternal/server/emerald/grpc_blockchain.goSubscribeNodeGroupStatushandlerinternal/upstreams/node_groups_test.gounknownfallback, caps/SubMethods excludedinternal/upstreams/chain_supervisor_groups_test.gointernal/server/emerald/sub_node_group_status_test.go*_test.go,pkg/test_utils/test_helpers.godocs/nodecore/12-grpc-server.mdSuggested review order
node_groups.gotop-to-bottom (identity → membership → recompute/heads).Concurrency model:
groups/upstreamGroup/members/fork choice aremutated only by the
processEventsgoroutine (same ownership aslastOver); cross-goroutine readers touch only theCMapand theper-group
Atomic.chain_supervisor.go— verify the network path is untouched apart from theextraction (the
recomputeStatebody is the oldupdateStateverbatim)and the four dispatch hooks.
sub_node_group_status.go— thegroupFullSentinvariant is stated abovethe goroutine: the map mirrors what the consumer knows; anything that
tells the consumer "unavailable" must clear it.
sub_node_group_status_test.goas the executable contract.Testing
go test -race -p 8 ./...,golangci-lint(CI config) clean, e2e tagcompiles; group-stream tests run repeatedly under
-race.upstreams on one instance, the group RPC announces exactly one full per real
node class (identical nodes aggregate into one group with
NodeDetailsquorum 2) and carries no merged frame, while
SubscribeChainStatuson thesame instance keeps returning only the merged view; per-block tagged deltas
and 1-min resync snapshots verified.
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.