From 57570852a8d273f16f2974e414a745852c29066d Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:19:44 +0800 Subject: [PATCH 01/11] Make use of TargetCommitteeSize in SeatManager --- .../confirmation_ratification.go | 2 +- .../thresholdblockgadget/witness_weight.go | 2 +- .../totalweightslotgadget/gadget.go | 2 +- .../orchestrator.go | 2 +- .../seatmanager/mock/mockseatmanager.go | 5 +- .../seatmanager/seatmanager.go | 4 +- .../seatmanager/topstakers/options.go | 6 -- .../seatmanager/topstakers/topstakers.go | 65 ++++++++---------- .../sybilprotectionv1/sybilprotection.go | 4 +- pkg/tests/committee_rotation_test.go | 37 +--------- pkg/tests/confirmation_state_test.go | 67 ++++++------------- pkg/testsuite/snapshotcreator/options.go | 20 ++---- .../snapshotcreator/snapshotcreator.go | 3 +- 13 files changed, 68 insertions(+), 151 deletions(-) diff --git a/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/confirmation_ratification.go b/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/confirmation_ratification.go index a86de875d..fc66ca885 100644 --- a/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/confirmation_ratification.go +++ b/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/confirmation_ratification.go @@ -51,7 +51,7 @@ func (g *Gadget) trackConfirmationRatifierWeight(votingBlock *blocks.Block) { func (g *Gadget) shouldConfirm(block *blocks.Block) bool { blockSeats := len(block.ConfirmationRatifiers()) - totalCommitteeSeats := g.seatManager.SeatCount() + totalCommitteeSeats := g.seatManager.SeatCountInSlot(block.ID().Slot()) return votes.IsThresholdReached(blockSeats, totalCommitteeSeats, g.optsConfirmationThreshold) } diff --git a/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/witness_weight.go b/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/witness_weight.go index 5faa0f703..3934e82d3 100644 --- a/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/witness_weight.go +++ b/pkg/protocol/engine/consensus/blockgadget/thresholdblockgadget/witness_weight.go @@ -87,7 +87,7 @@ func (g *Gadget) TrackWitnessWeight(votingBlock *blocks.Block) { } func (g *Gadget) shouldPreAcceptAndPreConfirm(block *blocks.Block) (preAccept bool, preConfirm bool) { - committeeTotalSeats := g.seatManager.SeatCount() + committeeTotalSeats := g.seatManager.SeatCountInSlot(block.ID().Slot()) blockSeats := len(block.Witnesses()) onlineCommitteeTotalSeats := g.seatManager.OnlineCommittee().Size() diff --git a/pkg/protocol/engine/consensus/slotgadget/totalweightslotgadget/gadget.go b/pkg/protocol/engine/consensus/slotgadget/totalweightslotgadget/gadget.go index 9dd33e104..fd50a630d 100644 --- a/pkg/protocol/engine/consensus/slotgadget/totalweightslotgadget/gadget.go +++ b/pkg/protocol/engine/consensus/slotgadget/totalweightslotgadget/gadget.go @@ -115,9 +115,9 @@ func (g *Gadget) trackVotes(block *blocks.Block) { } func (g *Gadget) refreshSlotFinalization(tracker *slottracker.SlotTracker, previousLatestSlotIndex iotago.SlotIndex, newLatestSlotIndex iotago.SlotIndex) (finalizedSlots []iotago.SlotIndex) { - committeeTotalSeats := g.seatManager.SeatCount() for i := lo.Max(g.lastFinalizedSlot, previousLatestSlotIndex) + 1; i <= newLatestSlotIndex; i++ { + committeeTotalSeats := g.seatManager.SeatCountInSlot(i) attestorsTotalSeats := len(tracker.Voters(i)) if !votes.IsThresholdReached(attestorsTotalSeats, committeeTotalSeats, g.optsSlotFinalizationThreshold) { diff --git a/pkg/protocol/engine/upgrade/signalingupgradeorchestrator/orchestrator.go b/pkg/protocol/engine/upgrade/signalingupgradeorchestrator/orchestrator.go index bca67f268..bf5cfb1bd 100644 --- a/pkg/protocol/engine/upgrade/signalingupgradeorchestrator/orchestrator.go +++ b/pkg/protocol/engine/upgrade/signalingupgradeorchestrator/orchestrator.go @@ -258,7 +258,7 @@ func (o *Orchestrator) tryUpgrade(currentEpoch iotago.EpochIndex, lastSlotInEpoc } // Check whether the threshold for version was reached. - totalSeatCount := o.seatManager.SeatCount() + totalSeatCount := o.seatManager.SeatCountInEpoch(currentEpoch) if !votes.IsThresholdReached(mostSupporters, totalSeatCount, votes.SuperMajority) { return } diff --git a/pkg/protocol/sybilprotection/seatmanager/mock/mockseatmanager.go b/pkg/protocol/sybilprotection/seatmanager/mock/mockseatmanager.go index 7e90c5bc4..021bc1374 100644 --- a/pkg/protocol/sybilprotection/seatmanager/mock/mockseatmanager.go +++ b/pkg/protocol/sybilprotection/seatmanager/mock/mockseatmanager.go @@ -159,7 +159,10 @@ func (m *ManualPOA) OnlineCommittee() ds.Set[account.SeatIndex] { return m.online } -func (m *ManualPOA) SeatCount() int { +func (m *ManualPOA) SeatCountInSlot(_ iotago.SlotIndex) int { + return m.committee.SeatCount() +} +func (m *ManualPOA) SeatCountInEpoch(_ iotago.EpochIndex) int { return m.committee.SeatCount() } diff --git a/pkg/protocol/sybilprotection/seatmanager/seatmanager.go b/pkg/protocol/sybilprotection/seatmanager/seatmanager.go index fe87322c4..38c443e17 100644 --- a/pkg/protocol/sybilprotection/seatmanager/seatmanager.go +++ b/pkg/protocol/sybilprotection/seatmanager/seatmanager.go @@ -33,7 +33,9 @@ type SeatManager interface { OnlineCommittee() ds.Set[account.SeatIndex] // SeatCount returns the number of seats in the SeatManager. - SeatCount() int + SeatCountInSlot(slot iotago.SlotIndex) int + + SeatCountInEpoch(epoch iotago.EpochIndex) int // Interface embeds the required methods of the module.Interface. module.Interface diff --git a/pkg/protocol/sybilprotection/seatmanager/topstakers/options.go b/pkg/protocol/sybilprotection/seatmanager/topstakers/options.go index 4f190f182..b6d6fffe4 100644 --- a/pkg/protocol/sybilprotection/seatmanager/topstakers/options.go +++ b/pkg/protocol/sybilprotection/seatmanager/topstakers/options.go @@ -19,9 +19,3 @@ func WithOnlineCommitteeStartup(optsOnlineCommittee ...iotago.AccountID) options p.optsOnlineCommitteeStartup = optsOnlineCommittee } } - -func WithSeatCount(optsSeatCount uint32) options.Option[SeatManager] { - return func(p *SeatManager) { - p.optsSeatCount = optsSeatCount - } -} diff --git a/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers.go b/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers.go index 464b83c9c..97986b0d3 100644 --- a/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers.go +++ b/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers.go @@ -7,6 +7,7 @@ import ( "github.com/iotaledger/hive.go/ds" "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/hive.go/runtime/module" "github.com/iotaledger/hive.go/runtime/options" "github.com/iotaledger/hive.go/runtime/syncutils" @@ -30,7 +31,6 @@ type SeatManager struct { committeeMutex syncutils.RWMutex activityTracker activitytracker.ActivityTracker - optsSeatCount uint32 optsActivityWindow time.Duration optsOnlineCommitteeStartup []iotago.AccountID @@ -85,42 +85,17 @@ func (s *SeatManager) RotateCommittee(epoch iotago.EpochIndex, candidates accoun s.committeeMutex.Lock() defer s.committeeMutex.Unlock() - // If there are fewer candidates than required for epoch 0, then the previous committee cannot be copied. - if len(candidates) < s.SeatCount() && epoch == 0 { - return nil, ierrors.Errorf("at least %d candidates are required for committee in epoch 0, got %d", s.SeatCount(), len(candidates)) - } - - // If there are fewer candidates than required, then re-use the previous committee. - if len(candidates) < s.SeatCount() { - // TODO: what if staking period of a committee member ends in the next epoch? - committee, exists := s.committeeInEpoch(epoch - 1) - if !exists { - return nil, ierrors.Errorf("cannot re-use previous committee from epoch %d as it does not exist", epoch-1) - } - - accounts, err := committee.Accounts() - if err != nil { - return nil, ierrors.Wrapf(err, "error while getting accounts from committee for epoch %d", epoch-1) - } - - if err := s.committeeStore.Store(epoch, accounts); err != nil { - return nil, ierrors.Wrapf(err, "error while storing committee for epoch %d", epoch) - } - - return committee, nil - } - - committee, err := s.selectNewCommittee(candidates) + committee, err := s.selectNewCommittee(epoch, candidates) if err != nil { return nil, ierrors.Wrap(err, "error while selecting new committee") } - accounts, err := committee.Accounts() + committeeAccounts, err := committee.Accounts() if err != nil { - return nil, ierrors.Wrapf(err, "error while getting accounts for newly selected committee for epoch %d", epoch) + return nil, ierrors.Wrapf(err, "error while getting committeeAccounts for newly selected committee for epoch %d", epoch) } - if err := s.committeeStore.Store(epoch, accounts); err != nil { + if err := s.committeeStore.Store(epoch, committeeAccounts); err != nil { return nil, ierrors.Wrapf(err, "error while storing committee for epoch %d", epoch) } @@ -161,8 +136,22 @@ func (s *SeatManager) OnlineCommittee() ds.Set[account.SeatIndex] { return s.activityTracker.OnlineCommittee() } -func (s *SeatManager) SeatCount() int { - return int(s.optsSeatCount) +func (s *SeatManager) SeatCountInSlot(slot iotago.SlotIndex) int { + epoch := s.apiProvider.APIForSlot(slot).TimeProvider().EpochFromSlot(slot) + + return s.SeatCountInEpoch(epoch) +} + +func (s *SeatManager) SeatCountInEpoch(epoch iotago.EpochIndex) int { + s.committeeMutex.RLock() + defer s.committeeMutex.RUnlock() + + // TODO: this function is a hot path as it is called for every single block. Maybe accessing the storage is too slow. + if committee, exists := s.committeeInEpoch(epoch); exists { + return committee.SeatCount() + } + + return int(s.apiProvider.APIForEpoch(epoch).ProtocolParameters().TargetCommitteeSize()) } func (s *SeatManager) Shutdown() { @@ -202,10 +191,6 @@ func (s *SeatManager) SetCommittee(epoch iotago.EpochIndex, validators *account. s.committeeMutex.Lock() defer s.committeeMutex.Unlock() - if validators.Size() != int(s.optsSeatCount) { - return ierrors.Errorf("invalid number of validators: %d, expected: %d", validators.Size(), s.optsSeatCount) - } - err := s.committeeStore.Store(epoch, validators) if err != nil { return ierrors.Wrapf(err, "failed to set committee for epoch %d", epoch) @@ -214,7 +199,7 @@ func (s *SeatManager) SetCommittee(epoch iotago.EpochIndex, validators *account. return nil } -func (s *SeatManager) selectNewCommittee(candidates accounts.AccountsData) (*account.SeatedAccounts, error) { +func (s *SeatManager) selectNewCommittee(epoch iotago.EpochIndex, candidates accounts.AccountsData) (*account.SeatedAccounts, error) { sort.Slice(candidates, func(i, j int) bool { // Prioritize the candidate that has a larger pool stake. if candidates[i].ValidatorStake+candidates[i].DelegationStake != candidates[j].ValidatorStake+candidates[j].DelegationStake { @@ -240,10 +225,14 @@ func (s *SeatManager) selectNewCommittee(candidates accounts.AccountsData) (*acc return bytes.Compare(candidates[i].ID[:], candidates[j].ID[:]) > 0 }) + // We try to select up to targetCommitteeSize candidates to be part of the committee. If there are fewer candidates + // than required, then we select all of them and the committee size will be smaller than targetCommitteeSize. + committeeSize := lo.Min(len(candidates), int(s.apiProvider.APIForEpoch(epoch).ProtocolParameters().TargetCommitteeSize())) + // Create new Accounts instance that only included validators selected to be part of the committee. newCommitteeAccounts := account.NewAccounts() - for _, candidateData := range candidates[:s.optsSeatCount] { + for _, candidateData := range candidates[:committeeSize] { if err := newCommitteeAccounts.Set(candidateData.ID, &account.Pool{ PoolStake: candidateData.ValidatorStake + candidateData.DelegationStake, ValidatorStake: candidateData.ValidatorStake, diff --git a/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go b/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go index 320f1e5cb..36b1dcfa5 100644 --- a/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go +++ b/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go @@ -18,7 +18,7 @@ import ( "github.com/iotaledger/iota-core/pkg/protocol/engine/ledger" "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection" "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/seatmanager" - "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/seatmanager/poa" + "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/seatmanager/topstakers" "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/sybilprotectionv1/performance" iotago "github.com/iotaledger/iota.go/v4" "github.com/iotaledger/iota.go/v4/nodeclient/apimodels" @@ -51,7 +51,7 @@ func NewProvider(opts ...options.Option[SybilProtection]) module.Provider[*engin events: sybilprotection.NewEvents(), apiProvider: e, - optsSeatManagerProvider: poa.NewProvider(), + optsSeatManagerProvider: topstakers.NewProvider(), }, opts, func(o *SybilProtection) { o.seatManager = o.optsSeatManagerProvider(e) diff --git a/pkg/tests/committee_rotation_test.go b/pkg/tests/committee_rotation_test.go index 835df5931..0dc594793 100644 --- a/pkg/tests/committee_rotation_test.go +++ b/pkg/tests/committee_rotation_test.go @@ -3,12 +3,7 @@ package tests import ( "testing" - "github.com/iotaledger/hive.go/runtime/options" - "github.com/iotaledger/iota-core/pkg/protocol" - "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/seatmanager/topstakers" - "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/sybilprotectionv1" "github.com/iotaledger/iota-core/pkg/testsuite" - "github.com/iotaledger/iota-core/pkg/testsuite/snapshotcreator" iotago "github.com/iotaledger/iota.go/v4" ) @@ -28,13 +23,7 @@ func Test_TopStakersRotation(t *testing.T) { 4, 5, ), - ), - testsuite.WithSnapshotOptions( - snapshotcreator.WithSeatManagerProvider( - topstakers.NewProvider( - topstakers.WithSeatCount(3), - ), - ), + iotago.WithTargetCommitteeSize(3), ), ) defer ts.Shutdown() @@ -47,30 +36,8 @@ func Test_TopStakersRotation(t *testing.T) { ts.AddValidatorNode("node6", 1_000_001) ts.AddGenesisWallet("default", node1) - nodeOptions := make(map[string][]options.Option[protocol.Protocol]) + ts.Run(true) - for _, node := range ts.Nodes() { - nodeOptions[node.Name] = []options.Option[protocol.Protocol]{protocol.WithSybilProtectionProvider( - sybilprotectionv1.NewProvider( - sybilprotectionv1.WithSeatManagerProvider( - topstakers.NewProvider( - topstakers.WithSeatCount(3), - ), - ), - ), - )} - } - ts.Run(true, nodeOptions) - - for _, node := range ts.Nodes() { - nodeOptions[node.Name] = []options.Option[protocol.Protocol]{protocol.WithSybilProtectionProvider( - sybilprotectionv1.NewProvider( - sybilprotectionv1.WithSeatManagerProvider( - topstakers.NewProvider(topstakers.WithSeatCount(3)), - ), - ), - )} - } ts.AssertSybilProtectionCommittee(0, []iotago.AccountID{ ts.Node("node1").Validator.AccountID, ts.Node("node2").Validator.AccountID, diff --git a/pkg/tests/confirmation_state_test.go b/pkg/tests/confirmation_state_test.go index 63219ee69..38428ad2b 100644 --- a/pkg/tests/confirmation_state_test.go +++ b/pkg/tests/confirmation_state_test.go @@ -9,7 +9,7 @@ import ( "github.com/iotaledger/hive.go/runtime/options" "github.com/iotaledger/iota-core/pkg/protocol" "github.com/iotaledger/iota-core/pkg/protocol/engine/notarization/slotnotarization" - "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/seatmanager/poa" + "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/seatmanager/topstakers" "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/sybilprotectionv1" "github.com/iotaledger/iota-core/pkg/testsuite" iotago "github.com/iotaledger/iota.go/v4" @@ -32,6 +32,7 @@ func TestConfirmationFlags(t *testing.T) { 20, testsuite.DefaultEpochNearingThreshold, ), + iotago.WithTargetCommitteeSize(4), ), ) defer ts.Shutdown() @@ -47,55 +48,27 @@ func TestConfirmationFlags(t *testing.T) { nodeC.Validator.AccountID, nodeD.Validator.AccountID, } - ts.Run(true, map[string][]options.Option[protocol.Protocol]{ - "nodeA": { - protocol.WithNotarizationProvider( - slotnotarization.NewProvider(), - ), - protocol.WithSybilProtectionProvider( - sybilprotectionv1.NewProvider( - sybilprotectionv1.WithSeatManagerProvider( - poa.NewProvider(poa.WithOnlineCommitteeStartup(nodeA.Validator.AccountID), poa.WithActivityWindow(2*time.Minute)), - ), - ), - ), - }, - "nodeB": { - protocol.WithNotarizationProvider( - slotnotarization.NewProvider(), - ), - protocol.WithSybilProtectionProvider( - sybilprotectionv1.NewProvider( - sybilprotectionv1.WithSeatManagerProvider( - poa.NewProvider(poa.WithOnlineCommitteeStartup(nodeA.Validator.AccountID), poa.WithActivityWindow(2*time.Minute)), - ), - ), - ), - }, - "nodeC": { - protocol.WithNotarizationProvider( - slotnotarization.NewProvider(), - ), - protocol.WithSybilProtectionProvider( - sybilprotectionv1.NewProvider( - sybilprotectionv1.WithSeatManagerProvider( - poa.NewProvider(poa.WithOnlineCommitteeStartup(nodeA.Validator.AccountID), poa.WithActivityWindow(2*time.Minute)), - ), - ), - ), - }, - "nodeD": { - protocol.WithNotarizationProvider( - slotnotarization.NewProvider(), - ), - protocol.WithSybilProtectionProvider( - sybilprotectionv1.NewProvider( - sybilprotectionv1.WithSeatManagerProvider( - poa.NewProvider(poa.WithOnlineCommitteeStartup(nodeA.Validator.AccountID), poa.WithActivityWindow(2*time.Minute)), + + nodeOpts := []options.Option[protocol.Protocol]{ + protocol.WithNotarizationProvider( + slotnotarization.NewProvider(), + ), + protocol.WithSybilProtectionProvider( + sybilprotectionv1.NewProvider( + sybilprotectionv1.WithSeatManagerProvider( + topstakers.NewProvider( + topstakers.WithOnlineCommitteeStartup(nodeA.Validator.AccountID), + topstakers.WithActivityWindow(2*time.Minute), ), ), ), - }, + ), + } + ts.Run(true, map[string][]options.Option[protocol.Protocol]{ + "nodeA": nodeOpts, + "nodeB": nodeOpts, + "nodeC": nodeOpts, + "nodeD": nodeOpts, }) // Verify that nodes have the expected states. diff --git a/pkg/testsuite/snapshotcreator/options.go b/pkg/testsuite/snapshotcreator/options.go index 20addc3f2..05ec414cf 100644 --- a/pkg/testsuite/snapshotcreator/options.go +++ b/pkg/testsuite/snapshotcreator/options.go @@ -6,8 +6,6 @@ import ( "github.com/iotaledger/iota-core/pkg/protocol/engine" "github.com/iotaledger/iota-core/pkg/protocol/engine/ledger" ledger1 "github.com/iotaledger/iota-core/pkg/protocol/engine/ledger/ledger" - "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/seatmanager" - "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/seatmanager/poa" "github.com/iotaledger/iota-core/pkg/testsuite/mock" iotago "github.com/iotaledger/iota.go/v4" ) @@ -35,26 +33,18 @@ type Options struct { // BasicOutput defines the basic outputs that are created in the ledger as part of the Genesis. BasicOutputs []BasicOutputDetails - DataBaseVersion byte - LedgerProvider module.Provider[*engine.Engine, ledger.Ledger] - SeatManagerProvider module.Provider[*engine.Engine, seatmanager.SeatManager] + DataBaseVersion byte + LedgerProvider module.Provider[*engine.Engine, ledger.Ledger] } func NewOptions(opts ...options.Option[Options]) *Options { return options.Apply(&Options{ - FilePath: "snapshot.bin", - DataBaseVersion: 1, - LedgerProvider: ledger1.NewProvider(), - SeatManagerProvider: poa.NewProvider(), + FilePath: "snapshot.bin", + DataBaseVersion: 1, + LedgerProvider: ledger1.NewProvider(), }, opts) } -func WithSeatManagerProvider(seatManagerProvider module.Provider[*engine.Engine, seatmanager.SeatManager]) options.Option[Options] { - return func(m *Options) { - m.SeatManagerProvider = seatManagerProvider - } -} - func WithLedgerProvider(ledgerProvider module.Provider[*engine.Engine, ledger.Ledger]) options.Option[Options] { return func(m *Options) { m.LedgerProvider = ledgerProvider diff --git a/pkg/testsuite/snapshotcreator/snapshotcreator.go b/pkg/testsuite/snapshotcreator/snapshotcreator.go index 52e546f34..ca4127824 100644 --- a/pkg/testsuite/snapshotcreator/snapshotcreator.go +++ b/pkg/testsuite/snapshotcreator/snapshotcreator.go @@ -106,8 +106,7 @@ func CreateSnapshot(opts ...options.Option[Options]) error { blocktime.NewProvider(), thresholdblockgadget.NewProvider(), totalweightslotgadget.NewProvider(), - sybilprotectionv1.NewProvider(sybilprotectionv1.WithInitialCommittee(committeeAccountsData), - sybilprotectionv1.WithSeatManagerProvider(opt.SeatManagerProvider)), + sybilprotectionv1.NewProvider(sybilprotectionv1.WithInitialCommittee(committeeAccountsData)), slotnotarization.NewProvider(), slotattestation.NewProvider(), opt.LedgerProvider, From ae8da071708fe1dc3b337749f826495c1c27d531 Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:20:14 +0800 Subject: [PATCH 02/11] Adjust unit tests of topstakers seatmanager --- .../seatmanager/topstakers/topstakers_test.go | 301 +++++++++++------- 1 file changed, 183 insertions(+), 118 deletions(-) diff --git a/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers_test.go b/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers_test.go index 54790e263..29de39ab1 100644 --- a/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers_test.go +++ b/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers_test.go @@ -1,6 +1,7 @@ package topstakers import ( + "fmt" "testing" "time" @@ -21,15 +22,22 @@ import ( ) func TestTopStakers_InitializeCommittee(t *testing.T) { + var testAPI = iotago.V3API( + iotago.NewV3ProtocolParameters( + iotago.WithNetworkOptions("TestJungle", "tgl"), + iotago.WithSupplyOptions(2_779_530_283_277_761, 0, 0, 0, 0, 0, 0), + iotago.WithWorkScoreOptions(0, 1, 0, 0, 0, 0, 0, 0, 0, 0), // all zero except block offset gives all blocks workscore = 1 + iotago.WithTargetCommitteeSize(3), + ), + ) + committeeStore := epochstore.NewStore(kvstore.Realm{}, kvstore.Realm{}, mapdb.NewMapDB(), 0, (*account.Accounts).Bytes, account.AccountsFromBytes) topStakersSeatManager := &SeatManager{ - apiProvider: api.SingleVersionProvider(tpkg.TestAPI), + apiProvider: api.SingleVersionProvider(testAPI), committeeStore: committeeStore, events: seatmanager.NewEvents(), activityTracker: activitytrackerv1.NewActivityTracker(time.Second * 30), - - optsSeatCount: 3, } // Create committee for epoch 0 @@ -58,161 +66,218 @@ func TestTopStakers_InitializeCommittee(t *testing.T) { } func TestTopStakers_RotateCommittee(t *testing.T) { + var testAPI = iotago.V3API( + iotago.NewV3ProtocolParameters( + iotago.WithNetworkOptions("TestJungle", "tgl"), + iotago.WithSupplyOptions(2_779_530_283_277_761, 0, 0, 0, 0, 0, 0), + iotago.WithWorkScoreOptions(0, 1, 0, 0, 0, 0, 0, 0, 0, 0), // all zero except block offset gives all blocks workscore = 1 + iotago.WithTargetCommitteeSize(10), + ), + ) + committeeStore := epochstore.NewStore(kvstore.Realm{}, kvstore.Realm{}, mapdb.NewMapDB(), 0, (*account.Accounts).Bytes, account.AccountsFromBytes) - topStakersSeatManager := &SeatManager{ - apiProvider: api.SingleVersionProvider(tpkg.TestAPI), + s := &SeatManager{ + apiProvider: api.SingleVersionProvider(testAPI), committeeStore: committeeStore, events: seatmanager.NewEvents(), activityTracker: activitytrackerv1.NewActivityTracker(time.Second * 30), - - optsSeatCount: 3, } // Committee should not exist because it was never set. - _, exists := topStakersSeatManager.CommitteeInSlot(10) + _, exists := s.CommitteeInSlot(10) require.False(t, exists) - _, exists = topStakersSeatManager.CommitteeInEpoch(0) + _, exists = s.CommitteeInEpoch(0) require.False(t, exists) + var committeeInEpoch0 *account.SeatedAccounts + var committeeInEpoch0IDs []iotago.AccountID + expectedCommitteeInEpoch0 := account.NewAccounts() + // Create committee for epoch 0 - initialCommittee := account.NewAccounts() - require.NoError(t, initialCommittee.Set(tpkg.RandAccountID(), &account.Pool{ - PoolStake: 1900, - ValidatorStake: 900, - FixedCost: 11, - })) - - require.NoError(t, initialCommittee.Set(tpkg.RandAccountID(), &account.Pool{ - PoolStake: 1900, - ValidatorStake: 900, - FixedCost: 11, - })) + { + addCommitteeMember(t, expectedCommitteeInEpoch0, &account.Pool{PoolStake: 1900, ValidatorStake: 900, FixedCost: 11}) + addCommitteeMember(t, expectedCommitteeInEpoch0, &account.Pool{PoolStake: 1900, ValidatorStake: 900, FixedCost: 11}) + addCommitteeMember(t, expectedCommitteeInEpoch0, &account.Pool{PoolStake: 1900, ValidatorStake: 900, FixedCost: 11}) - // Try setting committee that is too small - should return an error. - err := topStakersSeatManager.SetCommittee(0, initialCommittee) - require.Error(t, err) + // We should be able to set a committee with only 3 members for epoch 0 (this could be set e.g. via the snapshot). + err := s.SetCommittee(0, expectedCommitteeInEpoch0) + require.NoError(t, err) - require.NoError(t, initialCommittee.Set(tpkg.RandAccountID(), &account.Pool{ - PoolStake: 1900, - ValidatorStake: 900, - FixedCost: 11, - })) + // Make sure that the online committee is handled correctly. + { + committeeInEpoch0, exists = s.CommitteeInEpoch(0) + require.True(t, exists) + committeeInEpoch0IDs = expectedCommitteeInEpoch0.IDs() - // Set committee with the correct size - err = topStakersSeatManager.SetCommittee(0, initialCommittee) - require.NoError(t, err) - weightedSeats, exists := topStakersSeatManager.CommitteeInEpoch(0) - require.True(t, exists) - initialCommitteeAccountIDs := initialCommittee.IDs() + require.True(t, s.OnlineCommittee().IsEmpty()) - // Make sure that the online committee is handled correctly. - require.True(t, topStakersSeatManager.OnlineCommittee().IsEmpty()) + s.activityTracker.MarkSeatActive(lo.Return1(committeeInEpoch0.GetSeat(committeeInEpoch0IDs[0])), committeeInEpoch0IDs[0], testAPI.TimeProvider().SlotStartTime(1)) + assertOnlineCommittee(t, s.OnlineCommittee(), lo.Return1(committeeInEpoch0.GetSeat(committeeInEpoch0IDs[0]))) - topStakersSeatManager.activityTracker.MarkSeatActive(lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[0])), initialCommitteeAccountIDs[0], tpkg.TestAPI.TimeProvider().SlotStartTime(1)) - assertOnlineCommittee(t, topStakersSeatManager.OnlineCommittee(), lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[0]))) + s.activityTracker.MarkSeatActive(lo.Return1(committeeInEpoch0.GetSeat(committeeInEpoch0IDs[1])), committeeInEpoch0IDs[1], testAPI.TimeProvider().SlotStartTime(2)) + assertOnlineCommittee(t, s.OnlineCommittee(), lo.Return1(committeeInEpoch0.GetSeat(committeeInEpoch0IDs[0])), lo.Return1(committeeInEpoch0.GetSeat(committeeInEpoch0IDs[1]))) - topStakersSeatManager.activityTracker.MarkSeatActive(lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[1])), initialCommitteeAccountIDs[1], tpkg.TestAPI.TimeProvider().SlotStartTime(2)) - assertOnlineCommittee(t, topStakersSeatManager.OnlineCommittee(), lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[0])), lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[1]))) + s.activityTracker.MarkSeatActive(lo.Return1(committeeInEpoch0.GetSeat(committeeInEpoch0IDs[2])), committeeInEpoch0IDs[2], testAPI.TimeProvider().SlotStartTime(3)) + assertOnlineCommittee(t, s.OnlineCommittee(), lo.Return1(committeeInEpoch0.GetSeat(committeeInEpoch0IDs[0])), lo.Return1(committeeInEpoch0.GetSeat(committeeInEpoch0IDs[1])), lo.Return1(committeeInEpoch0.GetSeat(committeeInEpoch0IDs[2]))) - topStakersSeatManager.activityTracker.MarkSeatActive(lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[2])), initialCommitteeAccountIDs[2], tpkg.TestAPI.TimeProvider().SlotStartTime(3)) - assertOnlineCommittee(t, topStakersSeatManager.OnlineCommittee(), lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[0])), lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[1])), lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[2]))) + // Make sure that after a period of inactivity, the inactive seats are marked as offline. + s.activityTracker.MarkSeatActive(lo.Return1(committeeInEpoch0.GetSeat(committeeInEpoch0IDs[2])), committeeInEpoch0IDs[2], testAPI.TimeProvider().SlotEndTime(7)) + assertOnlineCommittee(t, s.OnlineCommittee(), lo.Return1(committeeInEpoch0.GetSeat(committeeInEpoch0IDs[2]))) + } - // Make sure that after a period of inactivity, the inactive seats are marked as offline. - topStakersSeatManager.activityTracker.MarkSeatActive(lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[2])), initialCommitteeAccountIDs[2], tpkg.TestAPI.TimeProvider().SlotEndTime(7)) - assertOnlineCommittee(t, topStakersSeatManager.OnlineCommittee(), lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[2]))) + // Make sure that the committee was assigned to the correct epoch. + _, exists = s.CommitteeInEpoch(1) + require.False(t, exists) - // Make sure that the committee was assigned to the correct epoch. - _, exists = topStakersSeatManager.CommitteeInEpoch(1) - require.False(t, exists) + // Make sure that the committee members match the expected ones. + assertCommitteeInEpoch(t, s, testAPI, 0, expectedCommitteeInEpoch0) - // Make sure that the committee members match the expected ones. - committee, exists := topStakersSeatManager.CommitteeInEpoch(0) - require.True(t, exists) - assertCommittee(t, initialCommittee, committee) + // Make sure that the committee size is correct for this epoch + assertCommitteeSizeInEpoch(t, s, testAPI, 0, 3) + } - committee, exists = topStakersSeatManager.CommitteeInSlot(3) - require.True(t, exists) - assertCommittee(t, initialCommittee, committee) - - // Design candidate list and expected committee members. - accountsContext := make(accounts.AccountsData, 0) - expectedCommittee := account.NewAccounts() - numCandidates := 10 - - // Add some candidates that have the same fields to test sorting by secondary fields. - candidate1ID := tpkg.RandAccountID() - accountsContext = append(accountsContext, &accounts.AccountData{ - ID: candidate1ID, - ValidatorStake: 399, - DelegationStake: 800 - 399, - FixedCost: 3, - StakeEndEpoch: iotago.MaxEpochIndex, - }) - - candidate2ID := tpkg.RandAccountID() - accountsContext = append(accountsContext, &accounts.AccountData{ - ID: candidate2ID, - ValidatorStake: 399, - DelegationStake: 800 - 399, - FixedCost: 3, - StakeEndEpoch: iotago.MaxEpochIndex, - }) - - for i := 1; i <= numCandidates; i++ { - candidateAccountID := tpkg.RandAccountID() - candidatePool := &account.Pool{ - PoolStake: iotago.BaseToken(i * 100), - ValidatorStake: iotago.BaseToken(i * 50), - FixedCost: iotago.Mana(i), + expectedCommitteeInEpoch1 := account.NewAccounts() + // Design candidate list and expected committee members for epoch 1. + { + epoch := iotago.EpochIndex(1) + accountsData := make(accounts.AccountsData, 0) + numCandidates := 15 + expectedCommitteeSize := testAPI.ProtocolParameters().TargetCommitteeSize() + require.EqualValues(t, expectedCommitteeSize, s.SeatCountInEpoch(epoch)) + + s.SeatCountInEpoch(epoch) + + // Add some candidates that have the same fields to test sorting by secondary fields. + { + candidate0ID := tpkg.RandAccountID() + candidate0ID.RegisterAlias("candidate0") + accountsData = append(accountsData, &accounts.AccountData{ + ID: candidate0ID, + ValidatorStake: 100, + DelegationStake: 800 - 399, + FixedCost: 3, + StakeEndEpoch: iotago.MaxEpochIndex, + }) + + candidate1ID := tpkg.RandAccountID() + candidate1ID.RegisterAlias("candidate1") + accountsData = append(accountsData, &accounts.AccountData{ + ID: candidate1ID, + ValidatorStake: 100, + DelegationStake: 800 - 399, + FixedCost: 3, + StakeEndEpoch: iotago.MaxEpochIndex, + }) } - accountsContext = append(accountsContext, &accounts.AccountData{ - ID: candidateAccountID, - ValidatorStake: iotago.BaseToken(i * 50), - DelegationStake: iotago.BaseToken(i*100) - iotago.BaseToken(i*50), - FixedCost: tpkg.RandMana(iotago.MaxMana), - StakeEndEpoch: tpkg.RandEpoch(), - }) - if i+topStakersSeatManager.SeatCount() > numCandidates { - expectedCommittee.Set(candidateAccountID, candidatePool) + for i := 2; i <= numCandidates; i++ { + candidateAccountID := tpkg.RandAccountID() + candidateAccountID.RegisterAlias(fmt.Sprintf("candidate%d", i)) + candidatePool := &account.Pool{ + PoolStake: iotago.BaseToken(i * 100), + ValidatorStake: iotago.BaseToken(i * 50), + FixedCost: iotago.Mana(i), + } + accountsData = append(accountsData, &accounts.AccountData{ + ID: candidateAccountID, + ValidatorStake: iotago.BaseToken(i * 50), + DelegationStake: iotago.BaseToken(i*100) - iotago.BaseToken(i*50), + FixedCost: tpkg.RandMana(iotago.MaxMana), + StakeEndEpoch: tpkg.RandEpoch(), + }) + + if i+int(expectedCommitteeSize) > numCandidates { + require.NoError(t, expectedCommitteeInEpoch1.Set(candidateAccountID, candidatePool)) + } } + + // Rotate the committee and make sure that the returned committee matches the expected. + rotatedCommitteeInEpoch1, err := s.RotateCommittee(epoch, accountsData) + require.NoError(t, err) + assertCommittee(t, expectedCommitteeInEpoch1, rotatedCommitteeInEpoch1) + + // Make sure that after committee rotation, the online committee is not changed. + assertOnlineCommittee(t, s.OnlineCommittee(), lo.Return1(committeeInEpoch0.GetSeat(committeeInEpoch0IDs[2]))) + + committeeInEpoch1Accounts, err := rotatedCommitteeInEpoch1.Accounts() + require.NoError(t, err) + newCommitteeMemberIDs := committeeInEpoch1Accounts.IDs() + + // A new committee member appears online and makes the previously active committee seat inactive. + s.activityTracker.MarkSeatActive(lo.Return1(committeeInEpoch0.GetSeat(newCommitteeMemberIDs[0])), newCommitteeMemberIDs[0], testAPI.TimeProvider().SlotEndTime(14)) + assertOnlineCommittee(t, s.OnlineCommittee(), lo.Return1(committeeInEpoch0.GetSeat(newCommitteeMemberIDs[0]))) + + // Make sure that the committee retrieved from the committee store matches the expected. + assertCommitteeInEpoch(t, s, testAPI, 1, expectedCommitteeInEpoch1) + assertCommitteeSizeInEpoch(t, s, testAPI, 1, 10) + + // Make sure that the previous committee was not modified and is still accessible. + assertCommitteeInEpoch(t, s, testAPI, 0, expectedCommitteeInEpoch0) + assertCommitteeSizeInEpoch(t, s, testAPI, 0, 3) } - // Rotate the committee and make sure that the returned committee matches the expected. - newCommittee, err := topStakersSeatManager.RotateCommittee(1, accountsContext) - require.NoError(t, err) - assertCommittee(t, expectedCommittee, newCommittee) + // Rotate committee again with fewer candidates than the target committee size. + { + epoch := iotago.EpochIndex(2) + accountsData := make(accounts.AccountsData, 0) + expectedCommitteeInEpoch2 := account.NewAccounts() + + candidate0ID := tpkg.RandAccountID() + candidate0ID.RegisterAlias("candidate0-epoch2") + accountsData = append(accountsData, &accounts.AccountData{ + ID: candidate0ID, + ValidatorStake: 100, + DelegationStake: 800 - 399, + FixedCost: 3, + StakeEndEpoch: iotago.MaxEpochIndex, + }) + require.NoError(t, expectedCommitteeInEpoch2.Set(candidate0ID, &account.Pool{PoolStake: 1900, ValidatorStake: 900, FixedCost: 11})) - // Make sure that after committee rotation, the online committee is not changed. - assertOnlineCommittee(t, topStakersSeatManager.OnlineCommittee(), lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[2]))) + // Rotate the committee and make sure that the returned committee matches the expected. + rotatedCommitteeInEpoch2, err := s.RotateCommittee(epoch, accountsData) + require.NoError(t, err) + assertCommittee(t, expectedCommitteeInEpoch2, rotatedCommitteeInEpoch2) - accounts, err := newCommittee.Accounts() - require.NoError(t, err) - newCommitteeMemberIDs := accounts.IDs() + assertCommitteeInEpoch(t, s, testAPI, 2, expectedCommitteeInEpoch2) + assertCommitteeSizeInEpoch(t, s, testAPI, 2, 1) - // A new committee member appears online and makes the previously active committee seat inactive. - topStakersSeatManager.activityTracker.MarkSeatActive(lo.Return1(weightedSeats.GetSeat(newCommitteeMemberIDs[0])), newCommitteeMemberIDs[0], tpkg.TestAPI.TimeProvider().SlotEndTime(14)) - assertOnlineCommittee(t, topStakersSeatManager.OnlineCommittee(), lo.Return1(weightedSeats.GetSeat(newCommitteeMemberIDs[0]))) + // Make sure that the committee retrieved from the committee store matches the expected. + assertCommitteeInEpoch(t, s, testAPI, 1, expectedCommitteeInEpoch1) + assertCommitteeSizeInEpoch(t, s, testAPI, 1, 10) - // Make sure that the committee retrieved from the committee store matches the expected. - committee, exists = topStakersSeatManager.CommitteeInEpoch(1) - require.True(t, exists) - assertCommittee(t, expectedCommittee, committee) + // Make sure that the previous committee was not modified and is still accessible. + assertCommitteeInEpoch(t, s, testAPI, 0, expectedCommitteeInEpoch0) + assertCommitteeSizeInEpoch(t, s, testAPI, 0, 3) + } +} + +func addCommitteeMember(t *testing.T, committee *account.Accounts, pool *account.Pool) iotago.AccountID { + accountID := tpkg.RandAccountID() + require.NoError(t, committee.Set(accountID, pool)) + + return accountID +} + +func assertCommitteeSizeInEpoch(t *testing.T, seatManager *SeatManager, testAPI iotago.API, epoch iotago.EpochIndex, expectedCommitteeSize int) { + require.Equal(t, expectedCommitteeSize, seatManager.SeatCountInEpoch(epoch)) + require.Equal(t, expectedCommitteeSize, seatManager.SeatCountInSlot(testAPI.TimeProvider().EpochStart(epoch))) + require.Equal(t, expectedCommitteeSize, seatManager.SeatCountInSlot(testAPI.TimeProvider().EpochEnd(epoch))) +} - committee, exists = topStakersSeatManager.CommitteeInSlot(tpkg.TestAPI.TimeProvider().EpochStart(1)) +func assertCommitteeInEpoch(t *testing.T, seatManager *SeatManager, testAPI iotago.API, epoch iotago.EpochIndex, expectedCommittee *account.Accounts) { + committee, exists := seatManager.CommitteeInEpoch(epoch) require.True(t, exists) assertCommittee(t, expectedCommittee, committee) - // Make sure that the previous committee was not modified and is still accessible. - committee, exists = topStakersSeatManager.CommitteeInEpoch(0) + committee, exists = seatManager.CommitteeInSlot(testAPI.TimeProvider().EpochStart(epoch)) require.True(t, exists) - assertCommittee(t, initialCommittee, committee) + assertCommittee(t, expectedCommittee, committee) - committee, exists = topStakersSeatManager.CommitteeInSlot(tpkg.TestAPI.TimeProvider().EpochEnd(0)) + committee, exists = seatManager.CommitteeInSlot(testAPI.TimeProvider().EpochEnd(epoch)) require.True(t, exists) - assertCommittee(t, initialCommittee, committee) + assertCommittee(t, expectedCommittee, committee) } func assertCommittee(t *testing.T, expectedCommittee *account.Accounts, actualCommittee *account.SeatedAccounts) { From 409f2e304b619301419e5565539bf123a5d70aa6 Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:24:41 +0800 Subject: [PATCH 03/11] Adjust POA to adhere to new SeatManager interface --- pkg/protocol/sybilprotection/seatmanager/poa/poa.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/protocol/sybilprotection/seatmanager/poa/poa.go b/pkg/protocol/sybilprotection/seatmanager/poa/poa.go index f5c113c76..5319928af 100644 --- a/pkg/protocol/sybilprotection/seatmanager/poa/poa.go +++ b/pkg/protocol/sybilprotection/seatmanager/poa/poa.go @@ -146,7 +146,14 @@ func (s *SeatManager) OnlineCommittee() ds.Set[account.SeatIndex] { return s.activityTracker.OnlineCommittee() } -func (s *SeatManager) SeatCount() int { +func (s *SeatManager) SeatCountInSlot(slot iotago.SlotIndex) int { + s.committeeMutex.RLock() + defer s.committeeMutex.RUnlock() + + return s.committee.SeatCount() +} + +func (s *SeatManager) SeatCountInEpoch(epoch iotago.EpochIndex) int { s.committeeMutex.RLock() defer s.committeeMutex.RUnlock() From 99d9bfbee89b8973f9250838465ae245dad77ce1 Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:40:01 +0800 Subject: [PATCH 04/11] please doggo --- pkg/protocol/sybilprotection/seatmanager/poa/poa.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/protocol/sybilprotection/seatmanager/poa/poa.go b/pkg/protocol/sybilprotection/seatmanager/poa/poa.go index 5319928af..ff2bed8dc 100644 --- a/pkg/protocol/sybilprotection/seatmanager/poa/poa.go +++ b/pkg/protocol/sybilprotection/seatmanager/poa/poa.go @@ -146,14 +146,14 @@ func (s *SeatManager) OnlineCommittee() ds.Set[account.SeatIndex] { return s.activityTracker.OnlineCommittee() } -func (s *SeatManager) SeatCountInSlot(slot iotago.SlotIndex) int { +func (s *SeatManager) SeatCountInSlot(_ iotago.SlotIndex) int { s.committeeMutex.RLock() defer s.committeeMutex.RUnlock() return s.committee.SeatCount() } -func (s *SeatManager) SeatCountInEpoch(epoch iotago.EpochIndex) int { +func (s *SeatManager) SeatCountInEpoch(_ iotago.EpochIndex) int { s.committeeMutex.RLock() defer s.committeeMutex.RUnlock() From d0fc7361d1f2082b00277f499017bbd3baf73d2b Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Mon, 13 Nov 2023 20:35:39 +0800 Subject: [PATCH 05/11] Change top stakers to error when a committee with size 0 is set --- .../seatmanager/topstakers/topstakers.go | 8 ++++ .../seatmanager/topstakers/topstakers_test.go | 46 +++++++++++++++++-- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers.go b/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers.go index e993ffe87..a56772599 100644 --- a/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers.go +++ b/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers.go @@ -85,6 +85,10 @@ func (s *SeatManager) RotateCommittee(epoch iotago.EpochIndex, candidates accoun s.committeeMutex.Lock() defer s.committeeMutex.Unlock() + if len(candidates) == 0 { + return nil, ierrors.New("candidates must not be empty") + } + committee, err := s.selectNewCommittee(epoch, candidates) if err != nil { return nil, ierrors.Wrap(err, "error while selecting new committee") @@ -191,6 +195,10 @@ func (s *SeatManager) SetCommittee(epoch iotago.EpochIndex, validators *account. s.committeeMutex.Lock() defer s.committeeMutex.Unlock() + if validators.Size() == 0 { + return ierrors.New("committee must not be empty") + } + err := s.committeeStore.Store(epoch, validators) if err != nil { return ierrors.Wrapf(err, "failed to set committee for epoch %d", epoch) diff --git a/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers_test.go b/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers_test.go index 29de39ab1..2e879fa08 100644 --- a/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers_test.go +++ b/pkg/protocol/sybilprotection/seatmanager/topstakers/topstakers_test.go @@ -40,6 +40,10 @@ func TestTopStakers_InitializeCommittee(t *testing.T) { activityTracker: activitytrackerv1.NewActivityTracker(time.Second * 30), } + // Try setting an empty committee. + err := topStakersSeatManager.SetCommittee(0, account.NewAccounts()) + require.Error(t, err) + // Create committee for epoch 0 initialCommittee := account.NewAccounts() for i := 0; i < 3; i++ { @@ -51,18 +55,24 @@ func TestTopStakers_InitializeCommittee(t *testing.T) { t.Fatal(err) } } - // Try setting committee that is too small - should return an error. - err := topStakersSeatManager.SetCommittee(0, initialCommittee) + + // Set committee for epoch 0. + err = topStakersSeatManager.SetCommittee(0, initialCommittee) require.NoError(t, err) weightedSeats, exists := topStakersSeatManager.CommitteeInEpoch(0) require.True(t, exists) initialCommitteeAccountIDs := initialCommittee.IDs() - // Make sure that the online committee is handled correctly. + // Online committee should be empty. require.True(t, topStakersSeatManager.OnlineCommittee().IsEmpty()) + // After initialization, the online committee should contain the seats of the initial committee. require.NoError(t, topStakersSeatManager.InitializeCommittee(0, time.Time{})) - assertOnlineCommittee(t, topStakersSeatManager.OnlineCommittee(), lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[0])), lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[2])), lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[2]))) + assertOnlineCommittee(t, topStakersSeatManager.OnlineCommittee(), + lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[0])), + lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[2])), + lo.Return1(weightedSeats.GetSeat(initialCommitteeAccountIDs[2])), + ) } func TestTopStakers_RotateCommittee(t *testing.T) { @@ -219,10 +229,10 @@ func TestTopStakers_RotateCommittee(t *testing.T) { } // Rotate committee again with fewer candidates than the target committee size. + expectedCommitteeInEpoch2 := account.NewAccounts() { epoch := iotago.EpochIndex(2) accountsData := make(accounts.AccountsData, 0) - expectedCommitteeInEpoch2 := account.NewAccounts() candidate0ID := tpkg.RandAccountID() candidate0ID.RegisterAlias("candidate0-epoch2") @@ -251,6 +261,32 @@ func TestTopStakers_RotateCommittee(t *testing.T) { assertCommitteeInEpoch(t, s, testAPI, 0, expectedCommitteeInEpoch0) assertCommitteeSizeInEpoch(t, s, testAPI, 0, 3) } + + // Try to rotate committee with no candidates. Instead, set reuse of committee. + { + epoch := iotago.EpochIndex(3) + accountsData := make(accounts.AccountsData, 0) + + _, err := s.RotateCommittee(epoch, accountsData) + require.Error(t, err) + + // Set reuse of committee manually. + expectedCommitteeInEpoch2.SetReused() + err = s.SetCommittee(epoch, expectedCommitteeInEpoch2) + require.NoError(t, err) + + assertCommitteeInEpoch(t, s, testAPI, 3, expectedCommitteeInEpoch2) + assertCommitteeSizeInEpoch(t, s, testAPI, 3, 1) + + assertCommitteeInEpoch(t, s, testAPI, 2, expectedCommitteeInEpoch2) + assertCommitteeSizeInEpoch(t, s, testAPI, 2, 1) + + // Make sure that the committee retrieved from the committee store matches the expected (with reused flag set). + loadedCommittee, err := s.committeeStore.Load(epoch) + require.NoError(t, err) + require.True(t, loadedCommittee.IsReused()) + assertCommittee(t, expectedCommitteeInEpoch2, loadedCommittee.SelectCommittee(loadedCommittee.IDs()...)) + } } func addCommitteeMember(t *testing.T, committee *account.Accounts, pool *account.Pool) iotago.AccountID { From 8c4f5639685bb3f75f8228676af05a78371e38f8 Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Mon, 13 Nov 2023 20:36:39 +0800 Subject: [PATCH 06/11] Optimize codepath to be ignored if not PayloadCandidacyAnnouncement --- .../sybilprotection/seatmanager/poa/poa.go | 6 +- .../performance/performance.go | 15 ++++- .../sybilprotectionv1/sybilprotection.go | 61 +++++++++++++------ 3 files changed, 60 insertions(+), 22 deletions(-) diff --git a/pkg/protocol/sybilprotection/seatmanager/poa/poa.go b/pkg/protocol/sybilprotection/seatmanager/poa/poa.go index ff2bed8dc..6b9c22133 100644 --- a/pkg/protocol/sybilprotection/seatmanager/poa/poa.go +++ b/pkg/protocol/sybilprotection/seatmanager/poa/poa.go @@ -173,9 +173,11 @@ func (s *SeatManager) InitializeCommittee(epoch iotago.EpochIndex, activityTime return ierrors.Wrapf(err, "failed to load PoA committee for epoch %d", epoch) } - s.committee = committeeAccounts.SelectCommittee(committeeAccounts.IDs()...) + committeeAccountsIDs := committeeAccounts.IDs() + s.committee = committeeAccounts.SelectCommittee(committeeAccountsIDs...) - onlineValidators := committeeAccounts.IDs() + // Set validators that are part of the committee as active. + onlineValidators := committeeAccountsIDs if len(s.optsOnlineCommitteeStartup) > 0 { onlineValidators = s.optsOnlineCommitteeStartup } diff --git a/pkg/protocol/sybilprotection/sybilprotectionv1/performance/performance.go b/pkg/protocol/sybilprotection/sybilprotectionv1/performance/performance.go index 6c855d4e4..4b487a865 100644 --- a/pkg/protocol/sybilprotection/sybilprotectionv1/performance/performance.go +++ b/pkg/protocol/sybilprotection/sybilprotectionv1/performance/performance.go @@ -34,7 +34,16 @@ type Tracker struct { mutex syncutils.RWMutex } -func NewTracker(rewardsStorePerEpochFunc func(epoch iotago.EpochIndex) (kvstore.KVStore, error), poolStatsStore *epochstore.Store[*model.PoolsStats], committeeStore *epochstore.Store[*account.Accounts], committeeCandidatesInEpochFunc func(epoch iotago.EpochIndex) (kvstore.KVStore, error), validatorPerformancesFunc func(slot iotago.SlotIndex) (*slotstore.Store[iotago.AccountID, *model.ValidatorPerformance], error), latestAppliedEpoch iotago.EpochIndex, apiProvider iotago.APIProvider, errHandler func(error)) *Tracker { +func NewTracker( + rewardsStorePerEpochFunc func(epoch iotago.EpochIndex) (kvstore.KVStore, error), + poolStatsStore *epochstore.Store[*model.PoolsStats], + committeeStore *epochstore.Store[*account.Accounts], + committeeCandidatesInEpochFunc func(epoch iotago.EpochIndex) (kvstore.KVStore, error), + validatorPerformancesFunc func(slot iotago.SlotIndex) (*slotstore.Store[iotago.AccountID, *model.ValidatorPerformance], error), + latestAppliedEpoch iotago.EpochIndex, + apiProvider iotago.APIProvider, + errHandler func(error), +) *Tracker { return &Tracker{ nextEpochCommitteeCandidates: shrinkingmap.New[iotago.AccountID, iotago.SlotIndex](), rewardsStorePerEpochFunc: rewardsStorePerEpochFunc, @@ -80,12 +89,12 @@ func (t *Tracker) TrackCandidateBlock(block *blocks.Block) { t.mutex.Lock() defer t.mutex.Unlock() - blockEpoch := t.apiProvider.APIForSlot(block.ID().Slot()).TimeProvider().EpochFromSlot(block.ID().Slot()) - if block.Payload().PayloadType() != iotago.PayloadCandidacyAnnouncement { return } + blockEpoch := t.apiProvider.APIForSlot(block.ID().Slot()).TimeProvider().EpochFromSlot(block.ID().Slot()) + var rollback bool t.nextEpochCommitteeCandidates.Compute(block.ProtocolBlock().Header.IssuerID, func(currentValue iotago.SlotIndex, exists bool) iotago.SlotIndex { if !exists || currentValue > block.ID().Slot() { diff --git a/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go b/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go index 57ac2aff6..446a29c34 100644 --- a/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go +++ b/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go @@ -106,6 +106,10 @@ func (o *SybilProtection) TrackBlock(block *blocks.Block) { return } + if block.Payload().PayloadType() != iotago.PayloadCandidacyAnnouncement { + return + } + accountData, exists, err := o.ledger.Account(block.ProtocolBlock().Header.IssuerID, block.SlotCommitmentID().Slot()) if err != nil { o.errHandler(ierrors.Wrapf(err, "error while retrieving account from account %s in slot %d from accounts ledger", block.ProtocolBlock().Header.IssuerID, block.SlotCommitmentID().Slot())) @@ -124,6 +128,7 @@ func (o *SybilProtection) TrackBlock(block *blocks.Block) { return } + fmt.Println("tracking block 1", block.ProtocolBlock().Header.IssuerID, block.ID(), accountData.StakeEndEpoch, blockEpoch, block.ID().Slot(), o.apiProvider.APIForSlot(block.ID().Slot()).TimeProvider().EpochEnd(blockEpoch)) // if a candidate block is issued in the stake end epoch, // or if block is issued after EpochEndSlot - EpochNearingThreshold, because candidates can register only until that point. // then don't consider it because the validator can't be part of the committee in the next epoch @@ -133,9 +138,8 @@ func (o *SybilProtection) TrackBlock(block *blocks.Block) { return } - if block.Payload().PayloadType() == iotago.PayloadCandidacyAnnouncement { - o.performanceTracker.TrackCandidateBlock(block) - } + fmt.Println("tracking block 2", block.ProtocolBlock().Header.IssuerID, block.ID(), accountData.StakeEndEpoch, blockEpoch, block.ID().Slot(), o.apiProvider.APIForSlot(block.ID().Slot()).TimeProvider().EpochEnd(blockEpoch)) + o.performanceTracker.TrackCandidateBlock(block) } func (o *SybilProtection) CommitSlot(slot iotago.SlotIndex) (committeeRoot iotago.Identifier, rewardsRoot iotago.Identifier, err error) { @@ -155,23 +159,12 @@ func (o *SybilProtection) CommitSlot(slot iotago.SlotIndex) (committeeRoot iotag if _, committeeExists := o.seatManager.CommitteeInEpoch(nextEpoch); !committeeExists { // If the committee for the epoch wasn't set before due to finalization of a slot, // we promote the current committee to also serve in the next epoch. - committee, exists := o.seatManager.CommitteeInEpoch(currentEpoch) - if !exists { - // that should never happen as it is already the fallback strategy - panic(fmt.Sprintf("committee for current epoch %d not found", currentEpoch)) - } - - committeeAccounts, err := committee.Accounts() + fmt.Println("reusing committee through MaxCommittableAge", nextEpoch, "slot", slot, maxCommittableAge) + committeeAccounts, err := o.reuseCommittee(currentEpoch, nextEpoch) if err != nil { - return iotago.Identifier{}, iotago.Identifier{}, ierrors.Wrapf(err, "failed to get accounts from committee for epoch %d", currentEpoch) + return iotago.Identifier{}, iotago.Identifier{}, ierrors.Wrapf(err, "failed to reuse committee for epoch %d", nextEpoch) } - committeeAccounts.SetReused() - if err = o.seatManager.SetCommittee(nextEpoch, committeeAccounts); err != nil { - return iotago.Identifier{}, iotago.Identifier{}, ierrors.Wrapf(err, "failed to set committee for epoch %d", nextEpoch) - } - o.performanceTracker.ClearCandidates() - o.events.CommitteeSelected.Trigger(committeeAccounts, nextEpoch) } } @@ -380,6 +373,29 @@ func (o *SybilProtection) OrderedRegisteredCandidateValidatorsList(epoch iotago. return validatorResp, nil } +func (o *SybilProtection) reuseCommittee(currentEpoch iotago.EpochIndex, targetEpoch iotago.EpochIndex) (*account.Accounts, error) { + committee, exists := o.seatManager.CommitteeInEpoch(currentEpoch) + if !exists { + // that should never happen as it is already the fallback strategy + panic(fmt.Sprintf("committee for current epoch %d not found", currentEpoch)) + } + + committeeAccounts, err := committee.Accounts() + if err != nil { + return nil, ierrors.Wrapf(err, "failed to get accounts from committee for epoch %d", currentEpoch) + } + + committeeAccounts.SetReused() + if err = o.seatManager.SetCommittee(targetEpoch, committeeAccounts); err != nil { + return nil, ierrors.Wrapf(err, "failed to set committee for epoch %d", targetEpoch) + } + + fmt.Println("clearing candidates for epoch", currentEpoch, targetEpoch) + o.performanceTracker.ClearCandidates() + + return committeeAccounts, nil +} + func (o *SybilProtection) selectNewCommittee(slot iotago.SlotIndex) (*account.Accounts, error) { timeProvider := o.apiProvider.APIForSlot(slot).TimeProvider() currentEpoch := timeProvider.EpochFromSlot(slot) @@ -389,6 +405,17 @@ func (o *SybilProtection) selectNewCommittee(slot iotago.SlotIndex) (*account.Ac return nil, ierrors.Wrapf(err, "failed to retrieve candidates for epoch %d", nextEpoch) } + // If there's no candidate, reuse the current committee. + if candidates.Size() == 0 { + fmt.Println("no candidates for epoch", nextEpoch, "reusing committee", currentEpoch, "slot", slot) + committeeAccounts, err := o.reuseCommittee(currentEpoch, nextEpoch) + if err != nil { + return nil, ierrors.Wrapf(err, "failed to reuse committee (due to no candidates) for epoch %d", nextEpoch) + } + + return committeeAccounts, nil + } + candidateAccounts := make(accounts.AccountsData, 0) if err := candidates.ForEach(func(candidate iotago.AccountID) error { accountData, exists, err := o.ledger.Account(candidate, slot) From 23ae614b7e31cca1e29cb051a22eed76315070a9 Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Mon, 13 Nov 2023 20:38:54 +0800 Subject: [PATCH 07/11] Implement end-to-end test for - committee rotation with candidacy announcements at different times - committee reuse due to no candidates announced but slot finalized - committee reuse due to no slot finalization at epochNearingThreshold - committee rotation to a smaller committee than TargetCommitteeSize --- pkg/tests/committee_rotation_test.go | 170 +++++++++++++++++++++------ pkg/tests/upgrade_signaling_test.go | 4 +- pkg/testsuite/sybilprotection.go | 12 +- 3 files changed, 142 insertions(+), 44 deletions(-) diff --git a/pkg/tests/committee_rotation_test.go b/pkg/tests/committee_rotation_test.go index 0dc594793..51a9ff03f 100644 --- a/pkg/tests/committee_rotation_test.go +++ b/pkg/tests/committee_rotation_test.go @@ -2,7 +2,13 @@ package tests import ( "testing" + "time" + "github.com/iotaledger/hive.go/runtime/options" + "github.com/iotaledger/iota-core/pkg/protocol" + "github.com/iotaledger/iota-core/pkg/protocol/engine/notarization/slotnotarization" + "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/seatmanager/topstakers" + "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/sybilprotectionv1" "github.com/iotaledger/iota-core/pkg/testsuite" iotago "github.com/iotaledger/iota.go/v4" ) @@ -19,7 +25,7 @@ func Test_TopStakersRotation(t *testing.T) { iotago.WithLivenessOptions( 10, 10, - 3, + 2, 4, 5, ), @@ -36,7 +42,33 @@ func Test_TopStakersRotation(t *testing.T) { ts.AddValidatorNode("node6", 1_000_001) ts.AddGenesisWallet("default", node1) - ts.Run(true) + ts.AddNode("node7") + + nodeOpts := []options.Option[protocol.Protocol]{ + protocol.WithNotarizationProvider( + slotnotarization.NewProvider(), + ), + protocol.WithSybilProtectionProvider( + sybilprotectionv1.NewProvider( + sybilprotectionv1.WithSeatManagerProvider( + topstakers.NewProvider( + // We need to make sure that inactive nodes are evicted from the committee to continue acceptance. + topstakers.WithActivityWindow(15 * time.Second), + ), + ), + ), + ), + } + + ts.Run(true, map[string][]options.Option[protocol.Protocol]{ + "node1": nodeOpts, + "node2": nodeOpts, + "node3": nodeOpts, + "node4": nodeOpts, + "node5": nodeOpts, + "node6": nodeOpts, + "node7": nodeOpts, + }) ts.AssertSybilProtectionCommittee(0, []iotago.AccountID{ ts.Node("node1").Validator.AccountID, @@ -44,41 +76,101 @@ func Test_TopStakersRotation(t *testing.T) { ts.Node("node3").Validator.AccountID, }, ts.Nodes()...) - ts.IssueBlocksAtSlots("wave-1:", []iotago.SlotIndex{1, 2, 3, 4}, 4, "Genesis", ts.Nodes(), true, nil) - - ts.IssueCandidacyAnnouncementInSlot("node1-candidacy:1", 4, "wave-1:4.3", ts.Wallet("node1")) - ts.IssueCandidacyAnnouncementInSlot("node4-candidacy:1", 5, "node1-candidacy:1", ts.Wallet("node4")) - - ts.IssueBlocksAtSlots("wave-2:", []iotago.SlotIndex{5, 6, 7, 8, 9}, 4, "node4-candidacy:1", ts.Nodes(), true, nil) - - ts.IssueCandidacyAnnouncementInSlot("node4-candidacy:2", 9, "wave-2:9.3", ts.Wallet("node4")) - ts.IssueCandidacyAnnouncementInSlot("node5-candidacy:1", 9, "node4-candidacy:2", ts.Wallet("node5")) - - // This candidacy should be considered as it's announced at the last possible slot. - ts.IssueCandidacyAnnouncementInSlot("node6-candidacy:1", 10, "node5-candidacy:1", ts.Wallet("node6")) - - ts.IssueBlocksAtSlots("wave-3:", []iotago.SlotIndex{10}, 4, "node6-candidacy:1", ts.Nodes(), true, nil) - - // Those candidacies should not be considered as they're issued after EpochNearingThreshold (slot 10). - ts.IssueCandidacyAnnouncementInSlot("node2-candidacy:1", 11, "wave-3:10.3", ts.Wallet("node2")) - ts.IssueCandidacyAnnouncementInSlot("node3-candidacy:1", 11, "node2-candidacy:1", ts.Wallet("node3")) - ts.IssueCandidacyAnnouncementInSlot("node4-candidacy:3", 11, "node3-candidacy:1", ts.Wallet("node3")) - ts.IssueCandidacyAnnouncementInSlot("node5-candidacy:2", 11, "node4-candidacy:3", ts.Wallet("node3")) - - // Assert that only candidates that issued before slot 11 are considered. - ts.AssertSybilProtectionCandidates(1, []iotago.AccountID{ - ts.Node("node1").Validator.AccountID, - ts.Node("node4").Validator.AccountID, - ts.Node("node5").Validator.AccountID, - ts.Node("node6").Validator.AccountID, - }, ts.Nodes()...) - - ts.IssueBlocksAtSlots("wave-4:", []iotago.SlotIndex{11, 12, 13, 14, 15, 16, 17}, 4, "node5-candidacy:2", ts.Nodes(), true, nil) - - ts.AssertLatestFinalizedSlot(13, ts.Nodes()...) - ts.AssertSybilProtectionCommittee(1, []iotago.AccountID{ - ts.Node("node1").Validator.AccountID, - ts.Node("node4").Validator.AccountID, - ts.Node("node5").Validator.AccountID, - }, ts.Nodes()...) + // Select committee for epoch 1 and test candidacy announcements at different times. + { + ts.IssueBlocksAtSlots("wave-1:", []iotago.SlotIndex{1, 2, 3, 4}, 4, "Genesis", ts.Nodes(), true, nil) + + ts.IssueCandidacyAnnouncementInSlot("node1-candidacy:1", 4, "wave-1:4.3", ts.Wallet("node1")) + ts.IssueCandidacyAnnouncementInSlot("node4-candidacy:1", 5, "node1-candidacy:1", ts.Wallet("node4")) + + ts.IssueBlocksAtSlots("wave-2:", []iotago.SlotIndex{5, 6, 7, 8, 9}, 4, "node4-candidacy:1", ts.Nodes(), true, nil) + + ts.IssueCandidacyAnnouncementInSlot("node4-candidacy:2", 9, "wave-2:9.3", ts.Wallet("node4")) + ts.IssueCandidacyAnnouncementInSlot("node5-candidacy:1", 9, "node4-candidacy:2", ts.Wallet("node5")) + + // This candidacy should be considered as it's announced at the last possible slot. + ts.IssueCandidacyAnnouncementInSlot("node6-candidacy:1", 10, "node5-candidacy:1", ts.Wallet("node6")) + + ts.IssueBlocksAtSlots("wave-3:", []iotago.SlotIndex{10}, 4, "node6-candidacy:1", ts.Nodes(), true, nil) + + // Those candidacies should not be considered as they're issued after EpochNearingThreshold (slot 10). + ts.IssueCandidacyAnnouncementInSlot("node2-candidacy:1", 11, "wave-3:10.3", ts.Wallet("node2")) + ts.IssueCandidacyAnnouncementInSlot("node3-candidacy:1", 11, "node2-candidacy:1", ts.Wallet("node3")) + ts.IssueCandidacyAnnouncementInSlot("node4-candidacy:3", 11, "node3-candidacy:1", ts.Wallet("node3")) + ts.IssueCandidacyAnnouncementInSlot("node5-candidacy:2", 11, "node4-candidacy:3", ts.Wallet("node3")) + + // Assert that only candidates that issued before slot 11 are considered. + ts.AssertSybilProtectionCandidates(1, []iotago.AccountID{ + ts.Node("node1").Validator.AccountID, + ts.Node("node4").Validator.AccountID, + ts.Node("node5").Validator.AccountID, + ts.Node("node6").Validator.AccountID, + }, ts.Nodes()...) + + ts.IssueBlocksAtSlots("wave-4:", []iotago.SlotIndex{11, 12, 13, 14, 15, 16, 17}, 4, "node5-candidacy:2", ts.Nodes(), true, nil) + + ts.AssertLatestFinalizedSlot(14, ts.Nodes()...) + ts.AssertSybilProtectionCommittee(1, []iotago.AccountID{ + ts.Node("node1").Validator.AccountID, + ts.Node("node4").Validator.AccountID, + ts.Node("node5").Validator.AccountID, + }, ts.Nodes()...) + } + + // Do not announce new candidacies for epoch 2 but finalize slots. The committee should be the reused. + { + ts.IssueBlocksAtSlots("wave-5:", []iotago.SlotIndex{18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30}, 4, "wave-4:17.3", ts.Nodes(), true, nil) + + ts.AssertSybilProtectionCandidates(2, []iotago.AccountID{}, ts.Nodes()...) + ts.AssertLatestCommitmentSlotIndex(28, ts.Nodes()...) + ts.AssertLatestFinalizedSlot(27, ts.Nodes()...) + ts.AssertSybilProtectionCommittee(2, []iotago.AccountID{ + ts.Node("node1").Validator.AccountID, + ts.Node("node4").Validator.AccountID, + ts.Node("node5").Validator.AccountID, + }, ts.Nodes()...) + } + + // Do not finalize slots in time for epoch 3. The committee should be the reused. Even though there are candidates. + { + // Issue blocks to remove the inactive committee members. + ts.IssueBlocksAtSlots("wave-6:", []iotago.SlotIndex{31, 32}, 4, "wave-5:30.3", ts.Nodes("node5", "node7"), false, nil) + ts.AssertLatestCommitmentSlotIndex(30, ts.Nodes()...) + + ts.IssueCandidacyAnnouncementInSlot("node6-candidacy:2", 33, "wave-6:32.3", ts.Wallet("node6")) + + // Issue the rest of the epoch just before we reach epoch end - maxCommittableAge. + ts.IssueBlocksAtSlots("wave-7:", []iotago.SlotIndex{33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45}, 4, "node6-candidacy:2", ts.Nodes("node5"), true, nil) + + ts.AssertLatestCommitmentSlotIndex(43, ts.Nodes()...) + // Even though we have a candidate, the committee should be reused as we did not finalize at epochNearingThreshold before epoch end - maxCommittableAge was committed + ts.AssertSybilProtectionCandidates(3, []iotago.AccountID{ + ts.Node("node6").Validator.AccountID, + }, ts.Nodes()...) + // Check that the committee is reused. + ts.AssertSybilProtectionCommittee(3, []iotago.AccountID{ + ts.Node("node1").Validator.AccountID, + ts.Node("node4").Validator.AccountID, + ts.Node("node5").Validator.AccountID, + }, ts.Nodes()...) + } + + // Rotate committee to smaller committee due to too few candidates available. + { + ts.IssueBlocksAtSlots("wave-8:", []iotago.SlotIndex{46, 47, 48, 49, 50, 51, 52, 53, 54, 55}, 4, "wave-7:45.3", ts.Nodes(), true, nil) + + ts.IssueCandidacyAnnouncementInSlot("node3-candidacy:2", 56, "wave-8:55.3", ts.Wallet("node3")) + + ts.IssueBlocksAtSlots("wave-8:", []iotago.SlotIndex{56, 57, 58, 59, 60, 61}, 4, "node3-candidacy:2", ts.Nodes(), true, nil) + + ts.AssertLatestCommitmentSlotIndex(59, ts.Nodes()...) + ts.AssertLatestFinalizedSlot(58, ts.Nodes()...) + // We finalized at epochEnd-epochNearingThreshold, so the committee should be rotated even if there is just one candidate. + ts.AssertSybilProtectionCandidates(4, []iotago.AccountID{ + ts.Node("node3").Validator.AccountID, + }, ts.Nodes()...) + ts.AssertSybilProtectionCommittee(4, []iotago.AccountID{ + ts.Node("node3").Validator.AccountID, + }, ts.Nodes()...) + } } diff --git a/pkg/tests/upgrade_signaling_test.go b/pkg/tests/upgrade_signaling_test.go index 3d69028c4..e9f1cd1da 100644 --- a/pkg/tests/upgrade_signaling_test.go +++ b/pkg/tests/upgrade_signaling_test.go @@ -40,8 +40,8 @@ func Test_Upgrade_Signaling(t *testing.T) { 10, 10, 2, - 6, - 2, + 4, + 5, ), iotago.WithVersionSignalingOptions(7, 5, 2), ), diff --git a/pkg/testsuite/sybilprotection.go b/pkg/testsuite/sybilprotection.go index 39673cb29..292b04ef9 100644 --- a/pkg/testsuite/sybilprotection.go +++ b/pkg/testsuite/sybilprotection.go @@ -17,11 +17,17 @@ func (t *TestSuite) AssertSybilProtectionCommittee(epoch iotago.EpochIndex, expe for _, node := range nodes { t.Eventually(func() error { - accounts, err := lo.Return1(node.Protocol.MainEngineInstance().SybilProtection.SeatManager().CommitteeInEpoch(epoch)).Accounts() + committeeInEpoch, exists := node.Protocol.MainEngineInstance().SybilProtection.SeatManager().CommitteeInEpoch(epoch) + if !exists { + return ierrors.Errorf("AssertSybilProtectionCommittee: %s: failed to get committee in epoch %d", node.Name, epoch) + } + + committeeInEpochAccounts, err := committeeInEpoch.Accounts() if err != nil { - t.Testing.Fatal(err) + return ierrors.Errorf("AssertSybilProtectionCommittee: %s: failed to get accounts in committee in epoch %d: %w", node.Name, epoch, err) } - accountIDs := accounts.IDs() + + accountIDs := committeeInEpochAccounts.IDs() if !assert.ElementsMatch(t.fakeTesting, expectedAccounts, accountIDs) { return ierrors.Errorf("AssertSybilProtectionCommittee: %s: expected %s, got %s", node.Name, expectedAccounts, accountIDs) } From b39ebeff43723d1abbd0856c100f14f17f343d68 Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:11:06 +0800 Subject: [PATCH 08/11] Adjust tests to sensible parameter combination of epochNearingThreshold and maxCommittableAge and fix broken tests --- pkg/tests/accounts_test.go | 8 ++++---- pkg/tests/confirmation_state_test.go | 4 ++-- pkg/tests/loss_of_acceptance_test.go | 6 +++--- pkg/tests/protocol_engine_switching_test.go | 2 +- pkg/tests/protocol_startup_test.go | 4 ++-- pkg/tests/upgrade_signaling_test.go | 21 +++++++++++++++++---- pkg/testsuite/testsuite_options.go | 2 +- 7 files changed, 30 insertions(+), 17 deletions(-) diff --git a/pkg/tests/accounts_test.go b/pkg/tests/accounts_test.go index 0cbf224e5..9e5b48556 100644 --- a/pkg/tests/accounts_test.go +++ b/pkg/tests/accounts_test.go @@ -40,7 +40,7 @@ func Test_TransitionAndDestroyAccount(t *testing.T) { testsuite.DefaultLivenessThresholdUpperBoundInSeconds, testsuite.DefaultMinCommittableAge, 100, - testsuite.DefaultEpochNearingThreshold, + 120, ), ), ) @@ -168,7 +168,7 @@ func Test_StakeDelegateAndDelayedClaim(t *testing.T) { testsuite.DefaultLivenessThresholdUpperBoundInSeconds, testsuite.DefaultMinCommittableAge, 100, - testsuite.DefaultEpochNearingThreshold, + 120, ), ), ) @@ -205,7 +205,7 @@ func Test_StakeDelegateAndDelayedClaim(t *testing.T) { BlockIssuerKeys: wallet.BlockIssuer.BlockIssuerKeys(), }, ts.Nodes()...) - //CREATE NEW ACCOUNT WITH BLOCK ISSUER AND STAKING FEATURES FROM BASIC UTXO + // CREATE NEW ACCOUNT WITH BLOCK ISSUER AND STAKING FEATURES FROM BASIC UTXO newAccountBlockIssuerKey := utils.RandBlockIssuerKey() // set the expiry slot of the transitioned genesis account to the latest committed + MaxCommittableAge newAccountExpirySlot := node1.Protocol.MainEngineInstance().Storage.Settings().LatestCommitment().Slot() + ts.API.ProtocolParameters().MaxCommittableAge() @@ -344,7 +344,7 @@ func Test_ImplicitAccounts(t *testing.T) { testsuite.DefaultLivenessThresholdUpperBoundInSeconds, testsuite.DefaultMinCommittableAge, 100, - testsuite.DefaultEpochNearingThreshold, + 120, ), ), ) diff --git a/pkg/tests/confirmation_state_test.go b/pkg/tests/confirmation_state_test.go index 38428ad2b..1c764d3bd 100644 --- a/pkg/tests/confirmation_state_test.go +++ b/pkg/tests/confirmation_state_test.go @@ -28,8 +28,8 @@ func TestConfirmationFlags(t *testing.T) { iotago.WithLivenessOptions( 10, 10, - 10, - 20, + testsuite.DefaultMinCommittableAge, + testsuite.DefaultMaxCommittableAge, testsuite.DefaultEpochNearingThreshold, ), iotago.WithTargetCommitteeSize(4), diff --git a/pkg/tests/loss_of_acceptance_test.go b/pkg/tests/loss_of_acceptance_test.go index 961adfbcd..789c48598 100644 --- a/pkg/tests/loss_of_acceptance_test.go +++ b/pkg/tests/loss_of_acceptance_test.go @@ -27,7 +27,7 @@ func TestLossOfAcceptanceFromGenesis(t *testing.T) { 10, 2, 4, - 2, + 5, ), ), ) @@ -113,7 +113,7 @@ func TestLossOfAcceptanceFromSnapshot(t *testing.T) { 10, 2, 4, - 2, + 5, ), ), ) @@ -208,7 +208,7 @@ func TestLossOfAcceptanceWithRestartFromDisk(t *testing.T) { 10, 2, 4, - 2, + 5, ), ), ) diff --git a/pkg/tests/protocol_engine_switching_test.go b/pkg/tests/protocol_engine_switching_test.go index b56f185ed..03b2cfc81 100644 --- a/pkg/tests/protocol_engine_switching_test.go +++ b/pkg/tests/protocol_engine_switching_test.go @@ -40,7 +40,7 @@ func TestProtocol_EngineSwitching(t *testing.T) { 10, 2, 4, - 2, + 5, ), ), diff --git a/pkg/tests/protocol_startup_test.go b/pkg/tests/protocol_startup_test.go index 8d1e502ed..e4124af5b 100644 --- a/pkg/tests/protocol_startup_test.go +++ b/pkg/tests/protocol_startup_test.go @@ -34,7 +34,7 @@ func Test_BookInCommittedSlot(t *testing.T) { 10, 2, 4, - 2, + 5, ), ), ) @@ -134,7 +134,7 @@ func Test_StartNodeFromSnapshotAndDisk(t *testing.T) { 10, 2, 4, - 2, + 5, ), ), ) diff --git a/pkg/tests/upgrade_signaling_test.go b/pkg/tests/upgrade_signaling_test.go index e9f1cd1da..76b63980f 100644 --- a/pkg/tests/upgrade_signaling_test.go +++ b/pkg/tests/upgrade_signaling_test.go @@ -19,6 +19,8 @@ import ( "github.com/iotaledger/iota-core/pkg/protocol/engine/accounts" "github.com/iotaledger/iota-core/pkg/protocol/engine/blocks" "github.com/iotaledger/iota-core/pkg/protocol/engine/upgrade/signalingupgradeorchestrator" + "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/seatmanager/topstakers" + "github.com/iotaledger/iota-core/pkg/protocol/sybilprotection/sybilprotectionv1" "github.com/iotaledger/iota-core/pkg/storage" "github.com/iotaledger/iota-core/pkg/storage/permanent" "github.com/iotaledger/iota-core/pkg/testsuite" @@ -84,6 +86,16 @@ func Test_Upgrade_Signaling(t *testing.T) { ), ), ), + protocol.WithSybilProtectionProvider( + sybilprotectionv1.NewProvider( + sybilprotectionv1.WithSeatManagerProvider( + topstakers.NewProvider( + // We need to make sure that inactive nodes are evicted from the committee to continue acceptance. + topstakers.WithActivityWindow(15 * time.Second), + ), + ), + ), + ), } nodeOptionsWithV5 := append(nodeOptionsWithoutV5, @@ -213,10 +225,10 @@ func Test_Upgrade_Signaling(t *testing.T) { }, ts.Nodes()...) // check that rollback is correct - account, exists, err := ts.Node("nodeA").Protocol.MainEngineInstance().Ledger.Account(ts.Node("nodeA").Validator.AccountID, 7) + pastAccounts, err := ts.Node("nodeA").Protocol.MainEngineInstance().Ledger.PastAccounts(iotago.AccountIDs{ts.Node("nodeA").Validator.AccountID}, 7) require.NoError(t, err) - require.True(t, exists) - require.Equal(t, model.VersionAndHash{Version: 4, Hash: hash2}, account.LatestSupportedProtocolVersionAndHash) + require.Contains(t, pastAccounts, ts.Node("nodeA").Validator.AccountID) + require.Equal(t, model.VersionAndHash{Version: 4, Hash: hash2}, pastAccounts[ts.Node("nodeA").Validator.AccountID].LatestSupportedProtocolVersionAndHash) ts.IssueBlocksAtEpoch("", 2, 4, "15.3", ts.Nodes(), true, nil) ts.IssueBlocksAtEpoch("", 3, 4, "23.3", ts.Nodes(), true, nil) @@ -399,7 +411,8 @@ func Test_Upgrade_Signaling(t *testing.T) { // Check that issuing still produces the same commitments on the nodes that upgraded. The nodes that did not upgrade // should not be able to issue and process blocks with the new version. - ts.IssueBlocksAtEpoch("", 8, 4, "63.3", ts.Nodes("nodeB", "nodeC"), false, nil) + ts.IssueBlocksAtSlots("", []iotago.SlotIndex{64, 65}, 4, "63.3", ts.Nodes("nodeB", "nodeC"), false, nil) + ts.IssueBlocksAtSlots("", []iotago.SlotIndex{66, 67, 68, 69, 70, 71}, 4, "65.3", ts.Nodes("nodeB", "nodeC"), true, nil) // Nodes that did not set up the new protocol parameters are not able to process blocks with the new version. ts.AssertNodeState(ts.Nodes("nodeA", "nodeD", "nodeF", "nodeG"), diff --git a/pkg/testsuite/testsuite_options.go b/pkg/testsuite/testsuite_options.go index 6f467834e..0f3991013 100644 --- a/pkg/testsuite/testsuite_options.go +++ b/pkg/testsuite/testsuite_options.go @@ -65,7 +65,7 @@ const ( DefaultLivenessThresholdUpperBoundInSeconds uint16 = 30 DefaultMinCommittableAge iotago.SlotIndex = 10 DefaultMaxCommittableAge iotago.SlotIndex = 20 - DefaultEpochNearingThreshold iotago.SlotIndex = 16 + DefaultEpochNearingThreshold iotago.SlotIndex = 24 DefaultMinReferenceManaCost iotago.Mana = 500 DefaultRMCIncrease iotago.Mana = 500 From 0e2e5595b8a6e9de737c4bc76f8b472cbd989dc6 Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:12:48 +0800 Subject: [PATCH 09/11] Remove debug prints --- .../sybilprotection/sybilprotectionv1/sybilprotection.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go b/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go index 446a29c34..124265349 100644 --- a/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go +++ b/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go @@ -128,7 +128,6 @@ func (o *SybilProtection) TrackBlock(block *blocks.Block) { return } - fmt.Println("tracking block 1", block.ProtocolBlock().Header.IssuerID, block.ID(), accountData.StakeEndEpoch, blockEpoch, block.ID().Slot(), o.apiProvider.APIForSlot(block.ID().Slot()).TimeProvider().EpochEnd(blockEpoch)) // if a candidate block is issued in the stake end epoch, // or if block is issued after EpochEndSlot - EpochNearingThreshold, because candidates can register only until that point. // then don't consider it because the validator can't be part of the committee in the next epoch @@ -138,7 +137,6 @@ func (o *SybilProtection) TrackBlock(block *blocks.Block) { return } - fmt.Println("tracking block 2", block.ProtocolBlock().Header.IssuerID, block.ID(), accountData.StakeEndEpoch, blockEpoch, block.ID().Slot(), o.apiProvider.APIForSlot(block.ID().Slot()).TimeProvider().EpochEnd(blockEpoch)) o.performanceTracker.TrackCandidateBlock(block) } @@ -159,7 +157,6 @@ func (o *SybilProtection) CommitSlot(slot iotago.SlotIndex) (committeeRoot iotag if _, committeeExists := o.seatManager.CommitteeInEpoch(nextEpoch); !committeeExists { // If the committee for the epoch wasn't set before due to finalization of a slot, // we promote the current committee to also serve in the next epoch. - fmt.Println("reusing committee through MaxCommittableAge", nextEpoch, "slot", slot, maxCommittableAge) committeeAccounts, err := o.reuseCommittee(currentEpoch, nextEpoch) if err != nil { return iotago.Identifier{}, iotago.Identifier{}, ierrors.Wrapf(err, "failed to reuse committee for epoch %d", nextEpoch) @@ -390,7 +387,6 @@ func (o *SybilProtection) reuseCommittee(currentEpoch iotago.EpochIndex, targetE return nil, ierrors.Wrapf(err, "failed to set committee for epoch %d", targetEpoch) } - fmt.Println("clearing candidates for epoch", currentEpoch, targetEpoch) o.performanceTracker.ClearCandidates() return committeeAccounts, nil @@ -407,7 +403,6 @@ func (o *SybilProtection) selectNewCommittee(slot iotago.SlotIndex) (*account.Ac // If there's no candidate, reuse the current committee. if candidates.Size() == 0 { - fmt.Println("no candidates for epoch", nextEpoch, "reusing committee", currentEpoch, "slot", slot) committeeAccounts, err := o.reuseCommittee(currentEpoch, nextEpoch) if err != nil { return nil, ierrors.Wrapf(err, "failed to reuse committee (due to no candidates) for epoch %d", nextEpoch) From 7d5ac6b26cdb6cbb326697859bfc83f6c7dbade1 Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Tue, 14 Nov 2023 10:38:39 +0800 Subject: [PATCH 10/11] Increase timeout for TestLossOfAcceptanceFromGenesis --- pkg/tests/loss_of_acceptance_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/tests/loss_of_acceptance_test.go b/pkg/tests/loss_of_acceptance_test.go index 789c48598..64c4a58c6 100644 --- a/pkg/tests/loss_of_acceptance_test.go +++ b/pkg/tests/loss_of_acceptance_test.go @@ -15,6 +15,7 @@ import ( func TestLossOfAcceptanceFromGenesis(t *testing.T) { ts := testsuite.NewTestSuite(t, + testsuite.WithWaitFor(15*time.Second), testsuite.WithProtocolParametersOptions( iotago.WithTimeProviderOptions( 0, From 756a6961aa868829f29ab80b152a37e516a58b59 Mon Sep 17 00:00:00 2001 From: jonastheis <4181434+jonastheis@users.noreply.github.com> Date: Tue, 14 Nov 2023 19:19:14 +0800 Subject: [PATCH 11/11] Fix getValidatorCandidates to get the registered candidates in the given epoch for the next epoch, adjust when we rotate the committee and tests accordingly --- .../performance/performance.go | 12 ++------ .../performance/tracker_test.go | 28 +++---------------- .../sybilprotectionv1/sybilprotection.go | 4 ++- pkg/tests/committee_rotation_test.go | 8 +++--- 4 files changed, 14 insertions(+), 38 deletions(-) diff --git a/pkg/protocol/sybilprotection/sybilprotectionv1/performance/performance.go b/pkg/protocol/sybilprotection/sybilprotectionv1/performance/performance.go index 4b487a865..9e11f42c7 100644 --- a/pkg/protocol/sybilprotection/sybilprotectionv1/performance/performance.go +++ b/pkg/protocol/sybilprotection/sybilprotectionv1/performance/performance.go @@ -134,6 +134,7 @@ func (t *Tracker) TrackCandidateBlock(block *blocks.Block) { } +// EligibleValidatorCandidates returns the eligible validator candidates registered in the given epoch for the next epoch. func (t *Tracker) EligibleValidatorCandidates(epoch iotago.EpochIndex) (ds.Set[iotago.AccountID], error) { t.mutex.RLock() defer t.mutex.RUnlock() @@ -141,7 +142,7 @@ func (t *Tracker) EligibleValidatorCandidates(epoch iotago.EpochIndex) (ds.Set[i return t.getValidatorCandidates(epoch) } -// ValidatorCandidates returns the registered validator candidates for the given epoch. +// ValidatorCandidates returns the eligible validator candidates registered in the given epoch for the next epoch. func (t *Tracker) ValidatorCandidates(epoch iotago.EpochIndex) (ds.Set[iotago.AccountID], error) { t.mutex.RLock() defer t.mutex.RUnlock() @@ -152,14 +153,7 @@ func (t *Tracker) ValidatorCandidates(epoch iotago.EpochIndex) (ds.Set[iotago.Ac func (t *Tracker) getValidatorCandidates(epoch iotago.EpochIndex) (ds.Set[iotago.AccountID], error) { candidates := ds.NewSet[iotago.AccountID]() - // Epoch 0 has no candidates as it's the genesis committee. - if epoch == 0 { - return candidates, nil - } - - // we store candidates in the store for the epoch of their activity, but the passed argument points to the target epoch, - // so it's necessary to subtract one epoch from the passed value - candidateStore, err := t.committeeCandidatesInEpochFunc(epoch - 1) + candidateStore, err := t.committeeCandidatesInEpochFunc(epoch) if err != nil { return nil, ierrors.Wrapf(err, "error while retrieving candidates for epoch %d", epoch) } diff --git a/pkg/protocol/sybilprotection/sybilprotectionv1/performance/tracker_test.go b/pkg/protocol/sybilprotection/sybilprotectionv1/performance/tracker_test.go index 1eddb125d..731e89acc 100644 --- a/pkg/protocol/sybilprotection/sybilprotectionv1/performance/tracker_test.go +++ b/pkg/protocol/sybilprotection/sybilprotectionv1/performance/tracker_test.go @@ -136,28 +136,8 @@ func TestManager_Candidates(t *testing.T) { ts.Instance.TrackCandidateBlock(blocks.NewBlock(lo.PanicOnErr(model.BlockFromBlock(block6)))) } - require.True(t, lo.PanicOnErr(ts.Instance.EligibleValidatorCandidates(1)).HasAll(ds.NewReadableSet(issuer1, issuer2, issuer3))) - require.True(t, lo.PanicOnErr(ts.Instance.ValidatorCandidates(1)).HasAll(ds.NewReadableSet(issuer1, issuer2, issuer3))) - require.True(t, lo.PanicOnErr(ts.Instance.EligibleValidatorCandidates(2)).IsEmpty()) - require.True(t, lo.PanicOnErr(ts.Instance.ValidatorCandidates(2)).IsEmpty()) - - // retrieve epoch candidates for epoch 0, because we candidates prefixed with epoch in which they candidated - candidatesStore, err := ts.Instance.committeeCandidatesInEpochFunc(0) - require.NoError(t, err) - - candidacySlotIssuer1, err := candidatesStore.Get(issuer1[:]) - require.NoError(t, err) - require.Equal(t, iotago.SlotIndex(1).MustBytes(), candidacySlotIssuer1) - - candidacySlotIssuer2, err := candidatesStore.Get(issuer2[:]) - require.NoError(t, err) - require.Equal(t, iotago.SlotIndex(2).MustBytes(), candidacySlotIssuer2) - - candidacySlotIssuer3, err := candidatesStore.Get(issuer3[:]) - require.NoError(t, err) - require.Equal(t, iotago.SlotIndex(3).MustBytes(), candidacySlotIssuer3) - - ts.Instance.ClearCandidates() - - require.True(t, ts.Instance.nextEpochCommitteeCandidates.IsEmpty()) + require.True(t, lo.PanicOnErr(ts.Instance.EligibleValidatorCandidates(0)).HasAll(ds.NewReadableSet(issuer1, issuer2, issuer3))) + require.True(t, lo.PanicOnErr(ts.Instance.ValidatorCandidates(0)).HasAll(ds.NewReadableSet(issuer1, issuer2, issuer3))) + require.True(t, lo.PanicOnErr(ts.Instance.EligibleValidatorCandidates(1)).IsEmpty()) + require.True(t, lo.PanicOnErr(ts.Instance.ValidatorCandidates(1)).IsEmpty()) } diff --git a/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go b/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go index af861c51f..48b167aef 100644 --- a/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go +++ b/pkg/protocol/sybilprotection/sybilprotectionv1/sybilprotection.go @@ -401,7 +401,9 @@ func (o *SybilProtection) selectNewCommittee(slot iotago.SlotIndex) (*account.Ac timeProvider := o.apiProvider.APIForSlot(slot).TimeProvider() currentEpoch := timeProvider.EpochFromSlot(slot) nextEpoch := currentEpoch + 1 - candidates, err := o.performanceTracker.EligibleValidatorCandidates(nextEpoch) + + // We get the list of candidates for the next epoch. They are registered in the current epoch. + candidates, err := o.performanceTracker.EligibleValidatorCandidates(currentEpoch) if err != nil { return nil, ierrors.Wrapf(err, "failed to retrieve candidates for epoch %d", nextEpoch) } diff --git a/pkg/tests/committee_rotation_test.go b/pkg/tests/committee_rotation_test.go index 51a9ff03f..a7415bc24 100644 --- a/pkg/tests/committee_rotation_test.go +++ b/pkg/tests/committee_rotation_test.go @@ -100,7 +100,7 @@ func Test_TopStakersRotation(t *testing.T) { ts.IssueCandidacyAnnouncementInSlot("node5-candidacy:2", 11, "node4-candidacy:3", ts.Wallet("node3")) // Assert that only candidates that issued before slot 11 are considered. - ts.AssertSybilProtectionCandidates(1, []iotago.AccountID{ + ts.AssertSybilProtectionCandidates(0, []iotago.AccountID{ ts.Node("node1").Validator.AccountID, ts.Node("node4").Validator.AccountID, ts.Node("node5").Validator.AccountID, @@ -121,7 +121,7 @@ func Test_TopStakersRotation(t *testing.T) { { ts.IssueBlocksAtSlots("wave-5:", []iotago.SlotIndex{18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30}, 4, "wave-4:17.3", ts.Nodes(), true, nil) - ts.AssertSybilProtectionCandidates(2, []iotago.AccountID{}, ts.Nodes()...) + ts.AssertSybilProtectionCandidates(1, []iotago.AccountID{}, ts.Nodes()...) ts.AssertLatestCommitmentSlotIndex(28, ts.Nodes()...) ts.AssertLatestFinalizedSlot(27, ts.Nodes()...) ts.AssertSybilProtectionCommittee(2, []iotago.AccountID{ @@ -144,7 +144,7 @@ func Test_TopStakersRotation(t *testing.T) { ts.AssertLatestCommitmentSlotIndex(43, ts.Nodes()...) // Even though we have a candidate, the committee should be reused as we did not finalize at epochNearingThreshold before epoch end - maxCommittableAge was committed - ts.AssertSybilProtectionCandidates(3, []iotago.AccountID{ + ts.AssertSybilProtectionCandidates(2, []iotago.AccountID{ ts.Node("node6").Validator.AccountID, }, ts.Nodes()...) // Check that the committee is reused. @@ -166,7 +166,7 @@ func Test_TopStakersRotation(t *testing.T) { ts.AssertLatestCommitmentSlotIndex(59, ts.Nodes()...) ts.AssertLatestFinalizedSlot(58, ts.Nodes()...) // We finalized at epochEnd-epochNearingThreshold, so the committee should be rotated even if there is just one candidate. - ts.AssertSybilProtectionCandidates(4, []iotago.AccountID{ + ts.AssertSybilProtectionCandidates(3, []iotago.AccountID{ ts.Node("node3").Validator.AccountID, }, ts.Nodes()...) ts.AssertSybilProtectionCommittee(4, []iotago.AccountID{