From 653159e18031c58e27a4b585cdd52670d2fe52e1 Mon Sep 17 00:00:00 2001 From: "Leo Zhang (zhangchiqing)" Date: Fri, 4 Sep 2026 17:14:33 -0700 Subject: [PATCH] Validate participant roles in IsValidEpochSetup IsValidEpochSetup already checked that an EpochSetup has enough active nodes for each known role, but it never rejected participants whose Role field was not a valid flow.Role. An invalid role (e.g. 0 or 42) would be accepted as long as the real roles were still represented, and later code paths that call Role.String() would panic on the bogus value. Add a defense-in-depth check that calls Role.Valid() for every participant and rejects the setup with an error if any participant has an invalid role. Also add a regression test that mutates a consensus participant to an invalid role and asserts that IsValidEpochSetup returns an error. --- state/protocol/validity.go | 8 ++++++++ state/protocol/validity_test.go | 16 ++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/state/protocol/validity.go b/state/protocol/validity.go index 8704a8290b7..fd610dff794 100644 --- a/state/protocol/validity.go +++ b/state/protocol/validity.go @@ -59,6 +59,7 @@ func IsValidEpochSetup(setup *flow.EpochSetup, verifyNetworkAddress bool) error // (a) each has a unique node ID, // (b) each has a unique network address (if `verifyNetworkAddress` is true), // (c) participants are sorted in canonical order. + // (d) each has a valid role. // Note that the system smart contracts manage the identity table as an unordered set! For the protocol state, we desire a fixed // ordering to simplify various implementation details, like the DKG. Therefore, we order identities in `flow.EpochSetup` during // conversion from cadence to Go in the function `convert.ServiceEvent(flow.ChainID, flow.Event)` in package `model/convert` @@ -86,6 +87,13 @@ func IsValidEpochSetup(setup *flow.EpochSetup, verifyNetworkAddress bool) error return fmt.Errorf("participants are not canonically ordered") } + // (d) each participant has a valid role + for _, participant := range setup.Participants { + if !participant.Role.Valid() { + return fmt.Errorf("invalid participant role (%d) for node (%x)", participant.Role, participant.NodeID) + } + } + // 3. CHECK: Enforce sufficient number of nodes for each role // IMPORTANT: here we remove all nodes with zero weight, as they are allowed to partake in communication but not in respective node functions activeParticipants := setup.Participants.Filter(filter.HasInitialWeight[flow.IdentitySkeleton](true)) diff --git a/state/protocol/validity_test.go b/state/protocol/validity_test.go index 363038f53fd..bff5f3551d4 100644 --- a/state/protocol/validity_test.go +++ b/state/protocol/validity_test.go @@ -68,6 +68,22 @@ func TestEpochSetupValidity(t *testing.T) { require.Error(t, err) }) + t.Run("invalid participant role", func(t *testing.T) { + _, result, _ := unittest.BootstrapFixture(participants) + setup := result.ServiceEvents[0].Event.(*flow.EpochSetup) + + // mutate a consensus node (not a collector, so cluster assignment stays valid) + for i, p := range setup.Participants { + if p.Role == flow.RoleConsensus { + setup.Participants[i].Role = flow.Role(42) + break + } + } + + err := protocol.IsValidEpochSetup(setup, true) + require.Error(t, err) + }) + t.Run("network addresses are not unique", func(t *testing.T) { _, result, _ := unittest.BootstrapFixture(participants) setup := result.ServiceEvents[0].Event.(*flow.EpochSetup)