From e3d0462b5ae8556673c58ba141d81ffb3b343318 Mon Sep 17 00:00:00 2001 From: Otto V Date: Mon, 15 Dec 2025 12:06:53 +0100 Subject: [PATCH 1/5] feat: Add configuration option to skip committee store operations --- fsm/committee.go | 35 +++++++++++++++++++++++------------ lib/config.go | 2 ++ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/fsm/committee.go b/fsm/committee.go index c5628623..3bed2e1c 100644 --- a/fsm/committee.go +++ b/fsm/committee.go @@ -288,9 +288,12 @@ func (s *StateMachine) UpdateCommittees(address crypto.AddressI, oldValidator *V func (s *StateMachine) SetCommittees(address crypto.AddressI, totalStake uint64, committees []uint64) (err lib.ErrorI) { // for each committee in the list for _, committee := range committees { - // set the address as a member - if err = s.SetCommitteeMember(address, committee, totalStake); err != nil { - return + //skip if height x or config.fsmconfig.skipcommittestore === true + if s.height <= 1020000 || !s.Config.SkipCommitteeStore { + // set the address as a member + if err = s.SetCommitteeMember(address, committee, totalStake); err != nil { + return + } } // add to the committee staked supply if err = s.AddToCommitteeSupplyForChain(committee, totalStake); err != nil { @@ -304,9 +307,11 @@ func (s *StateMachine) SetCommittees(address crypto.AddressI, totalStake uint64, func (s *StateMachine) DeleteCommittees(address crypto.AddressI, totalStake uint64, committees []uint64) (err lib.ErrorI) { // for each committee in the list for _, committee := range committees { - // remove the address from being a member - if err = s.DeleteCommitteeMember(address, committee, totalStake); err != nil { - return + if s.height <= 1020000 || !s.Config.SkipCommitteeStore { + // remove the address from being a member + if err = s.DeleteCommitteeMember(address, committee, totalStake); err != nil { + return + } } // subtract from the committee staked supply if err = s.SubFromCommitteeStakedSupplyForChain(committee, totalStake); err != nil { @@ -374,9 +379,12 @@ func (s *StateMachine) UpdateDelegations(address crypto.AddressI, oldValidator * // SetDelegations() sets the delegate 'membership' for an address, adding to the list and updating the supply pools func (s *StateMachine) SetDelegations(address crypto.AddressI, totalStake uint64, committees []uint64) lib.ErrorI { for _, committee := range committees { - // actually set the address in the delegate list - if err := s.SetDelegate(address, committee, totalStake); err != nil { - return err + + if s.height <= 1020000 || !s.Config.SkipCommitteeStore { + // actually set the address in the delegate list + if err := s.SetDelegate(address, committee, totalStake); err != nil { + return err + } } // add to the delegate supply (used for tracking amounts) if err := s.AddToDelegateSupplyForChain(committee, totalStake); err != nil { @@ -393,9 +401,12 @@ func (s *StateMachine) SetDelegations(address crypto.AddressI, totalStake uint64 // DeleteDelegations() removes the delegate 'membership' for an address, removing from the list and updating the supply pools func (s *StateMachine) DeleteDelegations(address crypto.AddressI, totalStake uint64, committees []uint64) lib.ErrorI { for _, committee := range committees { - // remove the address from the delegate list - if err := s.DeleteDelegate(address, committee, totalStake); err != nil { - return err + + if s.height <= 1020000 || !s.Config.SkipCommitteeStore { + // remove the address from the delegate list + if err := s.DeleteDelegate(address, committee, totalStake); err != nil { + return err + } } // remove from the delegate supply (used for tracking amounts) if err := s.SubFromDelegateStakedSupplyForChain(committee, totalStake); err != nil { diff --git a/lib/config.go b/lib/config.go index 963e3d34..d95628e2 100644 --- a/lib/config.go +++ b/lib/config.go @@ -147,6 +147,7 @@ const ( type StateMachineConfig struct { InitialTokensPerBlock uint64 `json:"initialTokensPerBlock"` // initial micro tokens minted per block (before halvenings) BlocksPerHalvening uint64 `json:"blocksPerHalvening"` // number of blocks between block reward halvings + SkipCommitteeStore bool `json:"skipCommitteeStore"` // if true, the committee store is not used } // DefaultStateMachineConfig returns FSM defaults @@ -154,6 +155,7 @@ func DefaultStateMachineConfig() StateMachineConfig { return StateMachineConfig{ InitialTokensPerBlock: DefaultInitialTokensPerBlock, BlocksPerHalvening: DefaultBlocksPerHalvening, + SkipCommitteeStore: false, } } From 22c54324cc4497c4a6dbd229a07d31e7e8c27025 Mon Sep 17 00:00:00 2001 From: Otto V Date: Fri, 19 Dec 2025 16:58:28 +0100 Subject: [PATCH 2/5] feat: Add configuration for skipping committee store operations and enhance state machine handling fix: enable validator filtering for committee ID 0 Resolved a bug where committee ID 0 was treated as "no filter," causing incorrect validator sets. Added a FilterByCommittee flag to explicitly trigger membership checks for the root chain while maintaining backward compatibility for non-zero IDs. --- .docker/volumes/node_1/config.json | 10 ++- .docker/volumes/node_2/config.json | 10 ++- .docker/volumes/node_3/config.json | 10 ++- fsm/committee.go | 107 +++++++++++++++++++---------- fsm/committee_test.go | 10 +++ fsm/message_test.go | 8 +++ fsm/state_test.go | 5 +- fsm/validator.go | 40 +++++++++-- fsm/validator_test.go | 61 +++++++++++----- lib/config.go | 29 ++++++-- lib/consensus.go | 9 +-- 11 files changed, 228 insertions(+), 71 deletions(-) diff --git a/.docker/volumes/node_1/config.json b/.docker/volumes/node_1/config.json index a50a90d9..13f4bd95 100644 --- a/.docker/volumes/node_1/config.json +++ b/.docker/volumes/node_1/config.json @@ -44,5 +44,13 @@ "individualMaxTxSize": 4000, "dropPercentage": 35, "metricsEnabled": true, - "prometheusAddress": "0.0.0.0:9090" + "prometheusAddress": "0.0.0.0:9090", + "initialTokensPerBlock": 80000000, + "blocksPerHalvening": 3150000, + "upgrades": { + "skipCommitteeStore": { + "enabled": true, + "activationHeight": 0 + } + } } diff --git a/.docker/volumes/node_2/config.json b/.docker/volumes/node_2/config.json index eaedd498..70fe841b 100644 --- a/.docker/volumes/node_2/config.json +++ b/.docker/volumes/node_2/config.json @@ -48,5 +48,13 @@ "individualMaxTxSize": 4000, "dropPercentage": 35, "metricsEnabled": true, - "prometheusAddress": "0.0.0.0:9091" + "prometheusAddress": "0.0.0.0:9091", + "initialTokensPerBlock": 80000000, + "blocksPerHalvening": 3150000, + "upgrades": { + "skipCommitteeStore": { + "enabled": true, + "activationHeight": 0 + } + } } diff --git a/.docker/volumes/node_3/config.json b/.docker/volumes/node_3/config.json index 93663a2e..28b67aa5 100755 --- a/.docker/volumes/node_3/config.json +++ b/.docker/volumes/node_3/config.json @@ -48,5 +48,13 @@ "individualMaxTxSize": 4000, "dropPercentage": 35, "metricsEnabled": true, - "prometheusAddress": "0.0.0.0:9090" + "prometheusAddress": "0.0.0.0:9090", + "initialTokensPerBlock": 80000000, + "blocksPerHalvening": 3150000, + "upgrades": { + "skipCommitteeStore": { + "enabled": true, + "activationHeight": 0 + } + } } diff --git a/fsm/committee.go b/fsm/committee.go index 3bed2e1c..a4722653 100644 --- a/fsm/committee.go +++ b/fsm/committee.go @@ -251,23 +251,34 @@ func (s *StateMachine) GetCommitteeMembers(chainId uint64) (vs lib.ValidatorSet, func (s *StateMachine) GetCommitteePaginated(p lib.PageParams, chainId uint64) (page *lib.Page, err lib.ErrorI) { // define a page and result variables page, res := lib.NewPage(p, ValidatorsPageName), make(ValidatorPage, 0) - // populate the page using an iterator over the 'committee prefix' ordered by stake (high to low) - err = page.Load(CommitteePrefix(chainId), true, &res, s.store, func(key, value []byte) (err lib.ErrorI) { - // get the address from the key - address, err := AddressFromKey(key) - if err != nil { - return err + // retrieve the full committee member set + vs, err := s.GetCommitteeMembers(chainId) + if err != nil { + // if there are no validators, return empty page instead of error + if err.Error() == lib.ErrNoValidators().Error() { + page.Results = &res + return page, nil } - // get the validator from the address - validator, err := s.GetValidator(address) - if err != nil { - return err + return nil, err + } + // populate the page using the consensus validator list + err = page.LoadArray(vs.ValidatorSet.ValidatorSet, &res, func(i any) (e lib.ErrorI) { + // cast to consensus validator + v, ok := i.(*lib.ConsensusValidator) + if !ok { + return lib.ErrInvalidArgument() } - // skip if validator is not paused and not unstaking - if validator.UnstakingHeight != 0 || validator.MaxPausedHeight != 0 { - return + // calculate the address from the public key + address, e := s.pubKeyBytesToAddress(v.PublicKey) + if e != nil { + return e + } + // get the full validator from the address + validator, e := s.GetValidator(crypto.NewAddress(address)) + if e != nil { + return e } - // append the validator to the page + // append the validator to the result res = append(res, validator) return }) @@ -286,14 +297,17 @@ func (s *StateMachine) UpdateCommittees(address crypto.AddressI, oldValidator *V // SetCommittees() sets the membership and staked supply for all an addresses' committees func (s *StateMachine) SetCommittees(address crypto.AddressI, totalStake uint64, committees []uint64) (err lib.ErrorI) { + // check if committee store operations should be skipped + skipStore := s.ShouldSkipCommitteeStore() // for each committee in the list for _, committee := range committees { - //skip if height x or config.fsmconfig.skipcommittestore === true - if s.height <= 1020000 || !s.Config.SkipCommitteeStore { + if !skipStore { // set the address as a member if err = s.SetCommitteeMember(address, committee, totalStake); err != nil { return } + } else { + s.log.Debugf("Skipping committee store write at height %d (activationHeight: %d)", s.height, s.Config.Upgrades.SkipCommitteeStore.ActivationHeight) } // add to the committee staked supply if err = s.AddToCommitteeSupplyForChain(committee, totalStake); err != nil { @@ -305,9 +319,11 @@ func (s *StateMachine) SetCommittees(address crypto.AddressI, totalStake uint64, // DeleteCommittees() deletes the membership and staked supply for each of an address' committees func (s *StateMachine) DeleteCommittees(address crypto.AddressI, totalStake uint64, committees []uint64) (err lib.ErrorI) { + // check if committee store operations should be skipped + skipStore := s.ShouldSkipCommitteeStore() // for each committee in the list for _, committee := range committees { - if s.height <= 1020000 || !s.Config.SkipCommitteeStore { + if !skipStore { // remove the address from being a member if err = s.DeleteCommitteeMember(address, committee, totalStake); err != nil { return @@ -331,6 +347,12 @@ func (s *StateMachine) DeleteCommitteeMember(address crypto.AddressI, chainId, s return s.Delete(KeyForCommittee(chainId, address, stakeForCommittee)) } +// ShouldSkipCommitteeStore returns true if the committee store operations should be skipped +func (s *StateMachine) ShouldSkipCommitteeStore() bool { + return s.Config.Upgrades.SkipCommitteeStore.Enabled && + s.height >= s.Config.Upgrades.SkipCommitteeStore.ActivationHeight +} + // DELEGATIONS BELOW // GetDelegates returns the active delegates for a given chainId. @@ -343,23 +365,34 @@ func (s *StateMachine) GetDelegates(chainId uint64) (vs lib.ValidatorSet, err li func (s *StateMachine) GetDelegatesPaginated(p lib.PageParams, chainId uint64) (page *lib.Page, err lib.ErrorI) { // create a page of validator objects page, res := lib.NewPage(p, ValidatorsPageName), make(ValidatorPage, 0) - // populate the page using the 'delegates' prefix sorted by stake (high to low) - err = page.Load(DelegatePrefix(chainId), true, &res, s.store, func(key, _ []byte) (err lib.ErrorI) { - // get the address from the key - address, err := AddressFromKey(key) - if err != nil { - return err + // retrieve the full delegate set + vs, err := s.GetDelegates(chainId) + if err != nil { + // if there are no validators, return empty page instead of error + if err.Error() == lib.ErrNoValidators().Error() { + page.Results = &res + return page, nil } - // get the validator from the address - validator, err := s.GetValidator(address) - if err != nil { - return err + return nil, err + } + // populate the page using the consensus validator list + err = page.LoadArray(vs.ValidatorSet.ValidatorSet, &res, func(i any) (e lib.ErrorI) { + // cast to consensus validator + v, ok := i.(*lib.ConsensusValidator) + if !ok { + return lib.ErrInvalidArgument() } - // skip if validator is paused or unstaking - if validator.UnstakingHeight != 0 || validator.MaxPausedHeight != 0 { - return + // calculate the address from the public key + address, e := s.pubKeyBytesToAddress(v.PublicKey) + if e != nil { + return e } - // append the validator to the page + // get the full validator from the address + validator, e := s.GetValidator(crypto.NewAddress(address)) + if e != nil { + return e + } + // append the validator to the result res = append(res, validator) return }) @@ -378,9 +411,11 @@ func (s *StateMachine) UpdateDelegations(address crypto.AddressI, oldValidator * // SetDelegations() sets the delegate 'membership' for an address, adding to the list and updating the supply pools func (s *StateMachine) SetDelegations(address crypto.AddressI, totalStake uint64, committees []uint64) lib.ErrorI { + // check if committee store operations should be skipped + skipStore := s.ShouldSkipCommitteeStore() + // for each committee in the list for _, committee := range committees { - - if s.height <= 1020000 || !s.Config.SkipCommitteeStore { + if !skipStore { // actually set the address in the delegate list if err := s.SetDelegate(address, committee, totalStake); err != nil { return err @@ -400,9 +435,11 @@ func (s *StateMachine) SetDelegations(address crypto.AddressI, totalStake uint64 // DeleteDelegations() removes the delegate 'membership' for an address, removing from the list and updating the supply pools func (s *StateMachine) DeleteDelegations(address crypto.AddressI, totalStake uint64, committees []uint64) lib.ErrorI { + // check if committee store operations should be skipped + skipStore := s.ShouldSkipCommitteeStore() + // for each committee in the list for _, committee := range committees { - - if s.height <= 1020000 || !s.Config.SkipCommitteeStore { + if !skipStore { // remove the address from the delegate list if err := s.DeleteDelegate(address, committee, totalStake); err != nil { return err diff --git a/fsm/committee_test.go b/fsm/committee_test.go index 0a650ba7..49a71216 100644 --- a/fsm/committee_test.go +++ b/fsm/committee_test.go @@ -1175,12 +1175,14 @@ func TestGetDelegatesPaginated(t *testing.T) { PublicKey: newTestPublicKeyBytes(t), StakedAmount: 1, Committees: []uint64{lib.CanopyChainId}, + Delegate: true, }, { Address: newTestAddressBytes(t, 1), PublicKey: newTestPublicKeyBytes(t, 1), StakedAmount: 2, Committees: []uint64{lib.CanopyChainId}, + Delegate: true, }, }, pageParams: lib.PageParams{ @@ -1198,12 +1200,14 @@ func TestGetDelegatesPaginated(t *testing.T) { PublicKey: newTestPublicKeyBytes(t), StakedAmount: 1, Committees: []uint64{lib.CanopyChainId}, + Delegate: true, }, { Address: newTestAddressBytes(t, 1), PublicKey: newTestPublicKeyBytes(t, 1), StakedAmount: 2, Committees: []uint64{lib.CanopyChainId}, + Delegate: true, }, }, pageParams: lib.PageParams{ @@ -1221,12 +1225,14 @@ func TestGetDelegatesPaginated(t *testing.T) { PublicKey: newTestPublicKeyBytes(t), StakedAmount: 1, Committees: []uint64{lib.CanopyChainId}, + Delegate: true, }, { Address: newTestAddressBytes(t, 1), PublicKey: newTestPublicKeyBytes(t, 1), StakedAmount: 2, Committees: []uint64{lib.CanopyChainId}, + Delegate: true, }, }, pageParams: lib.PageParams{ @@ -1383,6 +1389,10 @@ func TestUpdateDelegates(t *testing.T) { require.NoError(t, err) // run the function require.NoError(t, sm.UpdateDelegations(addr, val, v.StakedAmount, v.Committees)) + // update validator object with new committees and stake + val.StakedAmount = v.StakedAmount + val.Committees = v.Committees + require.NoError(t, sm.SetValidator(val)) } // for each expected committee for id, publicKeys := range test.expected { diff --git a/fsm/message_test.go b/fsm/message_test.go index a2f5eb2f..df8e03b2 100644 --- a/fsm/message_test.go +++ b/fsm/message_test.go @@ -1067,6 +1067,7 @@ func TestHandleMessageEditStake(t *testing.T) { detail: "the validator is updated but the balance and delegations remains the same", presetValidator: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: 1, Committees: []uint64{0, 1}, Output: newTestAddressBytes(t), @@ -1080,6 +1081,7 @@ func TestHandleMessageEditStake(t *testing.T) { }, expectedValidator: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: 1, Committees: []uint64{0, 1}, Output: newTestAddressBytes(t), @@ -1164,6 +1166,7 @@ func TestHandleMessageEditStake(t *testing.T) { detail: "the validator is updated with different delegations but the balance remains the same", presetValidator: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: 1, Committees: []uint64{0, 1}, Delegate: true, @@ -1175,6 +1178,7 @@ func TestHandleMessageEditStake(t *testing.T) { }, expectedValidator: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: 1, Committees: []uint64{1, 2, 3}, Delegate: true, @@ -1219,6 +1223,7 @@ func TestHandleMessageEditStake(t *testing.T) { presetSender: 2, presetValidator: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: 1, NetAddress: "tcp://example.com", Committees: []uint64{0, 1}, @@ -1232,6 +1237,7 @@ func TestHandleMessageEditStake(t *testing.T) { }, expectedValidator: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: 2, NetAddress: "tcp://example.com", Committees: []uint64{1, 2, 3}, @@ -1261,6 +1267,7 @@ func TestHandleMessageEditStake(t *testing.T) { presetSender: 2, presetValidator: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: 1, Committees: []uint64{0, 1}, Delegate: true, @@ -1273,6 +1280,7 @@ func TestHandleMessageEditStake(t *testing.T) { }, expectedValidator: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: 2, Committees: []uint64{1, 2, 3}, Delegate: true, diff --git a/fsm/state_test.go b/fsm/state_test.go index cf28db90..aefee8cc 100644 --- a/fsm/state_test.go +++ b/fsm/state_test.go @@ -310,6 +310,9 @@ func newTestStateMachine(t *testing.T) StateMachine { log := lib.NewDefaultLogger() db, err := store.NewStoreInMemory(log) require.NoError(t, err) + // Create state machine config with committee store skip disabled for tests + smConfig := lib.DefaultStateMachineConfig() + smConfig.Upgrades.SkipCommitteeStore.Enabled = false sm := StateMachine{ store: db, ProtocolVersion: 0, @@ -320,7 +323,7 @@ func newTestStateMachine(t *testing.T) StateMachine { proposeVoteConfig: AcceptAllProposals, Config: lib.Config{ MainConfig: lib.DefaultMainConfig(), - StateMachineConfig: lib.DefaultStateMachineConfig(), + StateMachineConfig: smConfig, }, events: new(lib.EventsTracker), log: log, diff --git a/fsm/validator.go b/fsm/validator.go index a152970d..1f6cfbf2 100644 --- a/fsm/validator.go +++ b/fsm/validator.go @@ -454,10 +454,11 @@ func (s *StateMachine) getValidatorSet(chainId uint64, delegate bool) (vs lib.Va for _, v := range validators { // exclude validators not part of the committee if !v.PassesFilter(lib.ValidatorFilters{ - Unstaking: lib.FilterOption_Exclude, - Paused: lib.FilterOption_Exclude, - Delegate: lib.FilterOption(delegateFilter), - Committee: chainId, + Unstaking: lib.FilterOption_Exclude, + Paused: lib.FilterOption_Exclude, + Delegate: lib.FilterOption(delegateFilter), + Committee: chainId, + FilterByCommittee: true, }) { continue } @@ -467,6 +468,34 @@ func (s *StateMachine) getValidatorSet(chainId uint64, delegate bool) (vs lib.Va } } }) + //// filter out validators not part of the committee + //filtered := slices.Collect(func(yield func(*Validator) bool) { + // for _, v := range validators { + // // exclude unstaking validators + // if v.UnstakingHeight != 0 { + // continue + // } + // // exclude paused validators + // if v.MaxPausedHeight != 0 { + // continue + // } + // // filter by delegate status + // if delegateFilter == lib.FilterOption_MustBe && !v.Delegate { + // continue + // } + // if delegateFilter == lib.FilterOption_Exclude && v.Delegate { + // continue + // } + // // filter by committee membership (always filter, including chainId 0) + // if !slices.Contains(v.Committees, chainId) { + // continue + // } + // // add validator to filtered list + // if !yield(v) { + // return + // } + // } + //}) // sort by highest stake then address slices.SortFunc(filtered, func(a, b *Validator) int { result := cmp.Compare(b.StakedAmount, a.StakedAmount) @@ -612,7 +641,8 @@ func (x *Validator) PassesFilter(f lib.ValidatorFilters) (ok bool) { return } } - if f.Committee != 0 { + // filter by committee if the ID is non-zero OR if explicitly requested (to handle chain 0) + if f.Committee != 0 || f.FilterByCommittee { if !slices.Contains(x.Committees, f.Committee) { return } diff --git a/fsm/validator_test.go b/fsm/validator_test.go index 15c4e672..4f9eb137 100644 --- a/fsm/validator_test.go +++ b/fsm/validator_test.go @@ -141,13 +141,13 @@ func TestSetGetValidators(t *testing.T) { }, { Address: newTestAddressBytes(t, 1), - PublicKey: newTestPublicKeyBytes(t), + PublicKey: newTestPublicKeyBytes(t, 1), StakedAmount: amount + 1, Committees: []uint64{lib.CanopyChainId, 2}, }, { Address: newTestAddressBytes(t, 2), - PublicKey: newTestPublicKeyBytes(t), + PublicKey: newTestPublicKeyBytes(t, 2), StakedAmount: amount, Committees: []uint64{lib.CanopyChainId, 2}, }, @@ -180,14 +180,14 @@ func TestSetGetValidators(t *testing.T) { }, { Address: newTestAddressBytes(t, 1), - PublicKey: newTestPublicKeyBytes(t), + PublicKey: newTestPublicKeyBytes(t, 1), StakedAmount: amount + 1, Delegate: true, Committees: []uint64{lib.CanopyChainId, 2}, }, { Address: newTestAddressBytes(t, 2), - PublicKey: newTestPublicKeyBytes(t), + PublicKey: newTestPublicKeyBytes(t, 2), StakedAmount: amount, Delegate: true, Committees: []uint64{lib.CanopyChainId, 2}, @@ -250,19 +250,23 @@ func TestSetGetValidators(t *testing.T) { for i, v := range got { require.EqualExportedValues(t, test.preset[i], v) } - // get the committees from state - set, err := sm.GetCommitteePaginated(lib.PageParams{}, lib.CanopyChainId) - require.NoError(t, err) - // check committees got vs expected - for i, member := range *set.Results.(*ValidatorPage) { - require.EqualExportedValues(t, test.preset[i], member) - } - // get delegates from state - set, err = sm.GetDelegatesPaginated(lib.PageParams{}, lib.CanopyChainId) - require.NoError(t, err) - // check delegates got vs expected - for i, member := range *set.Results.(*ValidatorPage) { - require.EqualExportedValues(t, test.preset[i], member) + // get the committees or delegates from state based on validator type + if len(test.preset) > 0 && test.preset[0].Delegate { + // get delegates from state + set, err := sm.GetDelegatesPaginated(lib.PageParams{}, lib.CanopyChainId) + require.NoError(t, err) + // check delegates got vs expected + for i, member := range *set.Results.(*ValidatorPage) { + require.EqualExportedValues(t, test.preset[i], member) + } + } else { + // get the committees from state + set, err := sm.GetCommitteePaginated(lib.PageParams{}, lib.CanopyChainId) + require.NoError(t, err) + // check committees got vs expected + for i, member := range *set.Results.(*ValidatorPage) { + require.EqualExportedValues(t, test.preset[i], member) + } } gotSupply, err := sm.GetSupply() require.NoError(t, err) @@ -388,11 +392,13 @@ func TestUpdateValidatorStake(t *testing.T) { detail: "no updates to the validator", preset: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: amount, Committees: []uint64{0, 1}, }, update: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: amount, Committees: []uint64{0, 1}, }, @@ -416,11 +422,13 @@ func TestUpdateValidatorStake(t *testing.T) { detail: "update validator stake", preset: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: amount, Committees: []uint64{0, 1}, }, update: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: amount + 1, Committees: []uint64{0, 1}, }, @@ -444,12 +452,14 @@ func TestUpdateValidatorStake(t *testing.T) { detail: "update delegate stake", preset: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: amount, Committees: []uint64{0, 1}, Delegate: true, }, update: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: amount + 1, Committees: []uint64{0, 1}, Delegate: true, @@ -485,11 +495,13 @@ func TestUpdateValidatorStake(t *testing.T) { detail: "update validator committees", preset: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: amount, Committees: []uint64{0, 1}, }, update: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: amount + 1, Committees: []uint64{0, 2}, }, @@ -513,12 +525,14 @@ func TestUpdateValidatorStake(t *testing.T) { detail: "update delegate committees", preset: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: amount, Committees: []uint64{0, 1}, Delegate: true, }, update: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: amount + 1, Committees: []uint64{0, 2}, Delegate: true, @@ -612,6 +626,7 @@ func TestDeleteValidator(t *testing.T) { detail: "delete validator with 1 committee", preset: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: amount, Committees: []uint64{0}, }, @@ -623,6 +638,7 @@ func TestDeleteValidator(t *testing.T) { detail: "delete validator with multiple committees", preset: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: amount, Committees: []uint64{0, 1, 2}, }, @@ -635,6 +651,7 @@ func TestDeleteValidator(t *testing.T) { detail: "delete delegate with 1 committee", preset: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: amount, Committees: []uint64{0, 1, 2}, Delegate: true, @@ -648,6 +665,7 @@ func TestDeleteValidator(t *testing.T) { detail: "delete delegate with multiple committees", preset: &Validator{ Address: newTestAddressBytes(t), + PublicKey: newTestPublicKeyBytes(t), StakedAmount: amount, Committees: []uint64{0, 1, 2}, Delegate: true, @@ -684,8 +702,13 @@ func TestDeleteValidator(t *testing.T) { // get the committee page, err = sm.GetCommitteePaginated(lib.PageParams{}, cId) } - require.NoError(t, err) - // ensure the slice contains the expected + // After deleting the only validator, the set may be empty which returns an error + // If there's an error, it should be "no validators in the set" + if err != nil { + require.ErrorContains(t, err, "there are no validators in the set") + continue + } + // ensure the slice doesn't contain the deleted validator var contains bool for _, member := range *page.Results.(*ValidatorPage) { if bytes.Equal(member.PublicKey, test.preset.PublicKey) { diff --git a/lib/config.go b/lib/config.go index d95628e2..c6c4997e 100644 --- a/lib/config.go +++ b/lib/config.go @@ -141,13 +141,29 @@ const ( DefaultInitialTokensPerBlock = uint64(80 * 1000000) // 80 CNPY // the number of blocks between each halvening (block reward is cut in half) event DefaultBlocksPerHalvening = uint64(3150000) // ~ 2 years - 20 second blocks + // default setting for skipping committee store operations (true = skip, recommended for new chains) + DefaultSkipCommitteeStoreEnabled = true + // default activation height for skip committee store upgrade (0 = from genesis) + DefaultSkipCommitteeStoreActivationHeight = uint64(0) ) // StateMachineConfig houses FSM level options type StateMachineConfig struct { - InitialTokensPerBlock uint64 `json:"initialTokensPerBlock"` // initial micro tokens minted per block (before halvenings) - BlocksPerHalvening uint64 `json:"blocksPerHalvening"` // number of blocks between block reward halvings - SkipCommitteeStore bool `json:"skipCommitteeStore"` // if true, the committee store is not used + InitialTokensPerBlock uint64 `json:"initialTokensPerBlock"` // initial micro tokens minted per block (before halvenings) + BlocksPerHalvening uint64 `json:"blocksPerHalvening"` // number of blocks between block reward halvings + Upgrades Upgrades `json:"upgrades"` // configuration for protocol upgrades and optimizations +} + +// Upgrades houses configuration for protocol upgrades and optimizations +// These are non-consensus breaking changes (for now) that allow nodes to optimize performance +type Upgrades struct { + SkipCommitteeStore UpgradeConfig `json:"skipCommitteeStore"` // skip committee store set/delete operations +} + +// UpgradeConfig defines when an upgrade activates +type UpgradeConfig struct { + Enabled bool `json:"enabled"` // if true, the upgrade is active + ActivationHeight uint64 `json:"activationHeight"` // block height when upgrade activates (0 = from genesis) } // DefaultStateMachineConfig returns FSM defaults @@ -155,7 +171,12 @@ func DefaultStateMachineConfig() StateMachineConfig { return StateMachineConfig{ InitialTokensPerBlock: DefaultInitialTokensPerBlock, BlocksPerHalvening: DefaultBlocksPerHalvening, - SkipCommitteeStore: false, + Upgrades: Upgrades{ + SkipCommitteeStore: UpgradeConfig{ + Enabled: DefaultSkipCommitteeStoreEnabled, + ActivationHeight: DefaultSkipCommitteeStoreActivationHeight, + }, + }, } } diff --git a/lib/consensus.go b/lib/consensus.go index 1c7ddacc..2d7478a2 100644 --- a/lib/consensus.go +++ b/lib/consensus.go @@ -376,10 +376,11 @@ func (x *ConsensusValidator) UnmarshalJSON(jsonBytes []byte) (err error) { // ValidatorFilters are used to filter types of validators from a ValidatorPage type ValidatorFilters struct { - Unstaking FilterOption `json:"unstaking"` // validators are currently unstaking - Paused FilterOption `json:"paused"` // validators are currently paused - Delegate FilterOption `json:"delegate"` // validators are set as delegates - Committee uint64 `json:"committee"` // validators are staked for this chain id (committee id) + Unstaking FilterOption `json:"unstaking"` + Paused FilterOption `json:"paused"` + Delegate FilterOption `json:"delegate"` + Committee uint64 `json:"committee"` + FilterByCommittee bool `json:"filter_by_committee"` // Add this flag } // On() returns whether there exists any filters From 087d922ffbfb0b79028c61ec460c472e319fc070 Mon Sep 17 00:00:00 2001 From: Otto V Date: Fri, 19 Dec 2025 16:59:33 +0100 Subject: [PATCH 3/5] remove commented out code --- fsm/validator.go | 29 +---------------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/fsm/validator.go b/fsm/validator.go index 1f6cfbf2..a604b3b6 100644 --- a/fsm/validator.go +++ b/fsm/validator.go @@ -468,34 +468,7 @@ func (s *StateMachine) getValidatorSet(chainId uint64, delegate bool) (vs lib.Va } } }) - //// filter out validators not part of the committee - //filtered := slices.Collect(func(yield func(*Validator) bool) { - // for _, v := range validators { - // // exclude unstaking validators - // if v.UnstakingHeight != 0 { - // continue - // } - // // exclude paused validators - // if v.MaxPausedHeight != 0 { - // continue - // } - // // filter by delegate status - // if delegateFilter == lib.FilterOption_MustBe && !v.Delegate { - // continue - // } - // if delegateFilter == lib.FilterOption_Exclude && v.Delegate { - // continue - // } - // // filter by committee membership (always filter, including chainId 0) - // if !slices.Contains(v.Committees, chainId) { - // continue - // } - // // add validator to filtered list - // if !yield(v) { - // return - // } - // } - //}) + // sort by highest stake then address slices.SortFunc(filtered, func(a, b *Validator) int { result := cmp.Compare(b.StakedAmount, a.StakedAmount) From c497e2be93408a7b08ca1fbb92c68527f1f963bc Mon Sep 17 00:00:00 2001 From: Otto V Date: Fri, 19 Dec 2025 17:53:06 +0100 Subject: [PATCH 4/5] remove debug log --- fsm/committee.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/fsm/committee.go b/fsm/committee.go index a4722653..1d78dce5 100644 --- a/fsm/committee.go +++ b/fsm/committee.go @@ -306,8 +306,6 @@ func (s *StateMachine) SetCommittees(address crypto.AddressI, totalStake uint64, if err = s.SetCommitteeMember(address, committee, totalStake); err != nil { return } - } else { - s.log.Debugf("Skipping committee store write at height %d (activationHeight: %d)", s.height, s.Config.Upgrades.SkipCommitteeStore.ActivationHeight) } // add to the committee staked supply if err = s.AddToCommitteeSupplyForChain(committee, totalStake); err != nil { From 4e70503cf443f92bad56711aaf6e6bd6d1e3695b Mon Sep 17 00:00:00 2001 From: Otto V Date: Fri, 19 Dec 2025 18:13:05 +0100 Subject: [PATCH 5/5] fix: Update JSON field name for committee filter option, Remove remmant from testing to use the default statemachineconfig --- fsm/state_test.go | 2 -- lib/consensus.go | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/fsm/state_test.go b/fsm/state_test.go index aefee8cc..5c05d262 100644 --- a/fsm/state_test.go +++ b/fsm/state_test.go @@ -310,9 +310,7 @@ func newTestStateMachine(t *testing.T) StateMachine { log := lib.NewDefaultLogger() db, err := store.NewStoreInMemory(log) require.NoError(t, err) - // Create state machine config with committee store skip disabled for tests smConfig := lib.DefaultStateMachineConfig() - smConfig.Upgrades.SkipCommitteeStore.Enabled = false sm := StateMachine{ store: db, ProtocolVersion: 0, diff --git a/lib/consensus.go b/lib/consensus.go index 2d7478a2..ea624b5f 100644 --- a/lib/consensus.go +++ b/lib/consensus.go @@ -380,7 +380,7 @@ type ValidatorFilters struct { Paused FilterOption `json:"paused"` Delegate FilterOption `json:"delegate"` Committee uint64 `json:"committee"` - FilterByCommittee bool `json:"filter_by_committee"` // Add this flag + FilterByCommittee bool `json:"filterByCommittee"` // Add this flag } // On() returns whether there exists any filters