diff --git a/kaiax/gov/headergov/impl/header.go b/kaiax/gov/headergov/impl/header.go index 86d94a8d1..077fbcf90 100644 --- a/kaiax/gov/headergov/impl/header.go +++ b/kaiax/gov/headergov/impl/header.go @@ -172,10 +172,22 @@ func (h *headerGovModule) checkConsistency(blockNum uint64, vote headergov.VoteD return err } - if slices.Contains(council, params.GoverningNode) { + if !slices.Contains(council, params.GoverningNode) { + return ErrGovNodeNotInValSetList + } + if !h.ChainConfig.IsPermissionlessForkEnabled(new(big.Int).SetUint64(blockNum)) { return nil } - return ErrGovNodeNotInValSetList + + // After Permissionless only the governing node may vote, so a successor outside the council could never vote again. + newNode, ok := vote.Value().(common.Address) + if !ok || common.EmptyAddress(newNode) { + return ErrInvalidKeyValue + } + if !slices.Contains(council, newNode) { + return ErrGovNodeNotInValSetList + } + return nil case gov.Kip71LowerBoundBaseFee: params := h.GetParamSet(blockNum) if vote.Value().(uint64) > params.UpperBoundBaseFee { diff --git a/kaiax/gov/headergov/impl/header_test.go b/kaiax/gov/headergov/impl/header_test.go index 7516b147d..9912867b4 100644 --- a/kaiax/gov/headergov/impl/header_test.go +++ b/kaiax/gov/headergov/impl/header_test.go @@ -143,6 +143,40 @@ func TestVerifyVote_SingleMode(t *testing.T) { }) } +// A successor outside the council could never vote after Permissionless, since only the +// governing node may vote from then on. +func TestVerifyVote_GoverningNodeSuccessor(t *testing.T) { + tcs := []struct { + desc string + permissionless bool + governingNode common.Address + successor common.Address + expectedError error + }{ + {"council member", true, validVoter, validVoter, nil}, + {"zero address", true, validVoter, common.Address{}, ErrInvalidKeyValue}, + {"outside the council", true, validVoter, common.Address{9}, ErrGovNodeNotInValSetList}, + {"pre-permissionless accepts any address", false, validVoter, common.Address{}, nil}, + {"governing node outside the council", false, common.Address{2}, validVoter, ErrGovNodeNotInValSetList}, + } + for _, tc := range tcs { + t.Run(tc.desc, func(t *testing.T) { + config := getTestChainConfig() + config.Governance.GoverningNode = tc.governingNode + if tc.permissionless { + config.PermissionlessCompatibleBlock = common.Big0 + } else { + config.PermissionlessCompatibleBlock = nil + } + h := newHeaderGovModule(t, config) + vote := headergov.NewVoteData(validVoter, string(gov.GovernanceGoverningNode), tc.successor) + vb, err := vote.ToVoteBytes() + require.NoError(t, err) + assert.Equal(t, tc.expectedError, h.VerifyVote(&types.Header{Number: big.NewInt(1), Vote: vb, Extra: extra})) + }) + } +} + func TestGetVotesInEpoch(t *testing.T) { h := newHeaderGovModule(t, getTestChainConfig())