diff options
-rw-r--r-- | core/test/governance.go | 77 | ||||
-rw-r--r-- | core/test/governance_test.go | 30 | ||||
-rw-r--r-- | core/test/state.go | 15 |
3 files changed, 113 insertions, 9 deletions
diff --git a/core/test/governance.go b/core/test/governance.go index 4a2b61d..f07e7da 100644 --- a/core/test/governance.go +++ b/core/test/governance.go @@ -19,6 +19,8 @@ package test import ( "encoding/hex" + "errors" + "fmt" "reflect" "sort" "sync" @@ -33,10 +35,11 @@ import ( // Governance is an implementation of Goverance for testing purpose. type Governance struct { - configs []*types.Config - nodeSets [][]crypto.PublicKey - state *State - lock sync.RWMutex + configs []*types.Config + nodeSets [][]crypto.PublicKey + state *State + pendingConfigChanges map[uint64]map[StateChangeType]interface{} + lock sync.RWMutex } // NewGovernance constructs a Governance instance. @@ -47,7 +50,8 @@ func NewGovernance(genesisNodes []crypto.PublicKey, // public class in another, I did this to make the range of // modification smaller. g = &Governance{ - state: NewState(genesisNodes, lambda, true), + pendingConfigChanges: make(map[uint64]map[StateChangeType]interface{}), + state: NewState(genesisNodes, lambda, true), } return } @@ -77,7 +81,7 @@ func (g *Governance) Configuration(round uint64) *types.Config { } g.lock.RLock() defer g.lock.RUnlock() - if round >= uint64(len(g.nodeSets)) { + if round >= uint64(len(g.configs)) { return nil } return g.configs[round] @@ -91,6 +95,17 @@ func (g *Governance) CRS(round uint64) common.Hash { // NotifyRoundHeight notifies governace contract to snapshot config. func (g *Governance) NotifyRoundHeight(round, height uint64) { g.CatchUpWithRound(round) + // Apply change request for next round. + func() { + g.lock.Lock() + defer g.lock.Unlock() + for t, v := range g.pendingConfigChanges[round+1] { + if err := g.state.RequestChange(t, v); err != nil { + panic(err) + } + } + delete(g.pendingConfigChanges, round+1) + }() } // ProposeCRS propose a CRS. @@ -202,6 +217,14 @@ func (g *Governance) Clone() *Governance { copiedConfigs = append(copiedConfigs, c.Clone()) } // Clone node sets. + copiedPendingChanges := make(map[uint64]map[StateChangeType]interface{}) + for round, forRound := range g.pendingConfigChanges { + copiedForRound := make(map[StateChangeType]interface{}) + for k, v := range forRound { + copiedForRound[k] = v + } + copiedPendingChanges[round] = copiedForRound + } // NOTE: here I assume the key is from ecdsa. copiedNodeSets := [][]crypto.PublicKey{} for _, nodeSetForRound := range g.nodeSets { @@ -215,10 +238,13 @@ func (g *Governance) Clone() *Governance { } copiedNodeSets = append(copiedNodeSets, copiedNodeSet) } + // Clone pending changes. + return &Governance{ - configs: copiedConfigs, - state: copiedState, - nodeSets: copiedNodeSets, + configs: copiedConfigs, + state: copiedState, + nodeSets: copiedNodeSets, + pendingConfigChanges: copiedPendingChanges, } } @@ -232,6 +258,10 @@ func (g *Governance) Equal(other *Governance, checkState bool) bool { if len(g.nodeSets) != len(other.nodeSets) { return false } + // Check pending changes. + if !reflect.DeepEqual(g.pendingConfigChanges, other.pendingConfigChanges) { + return false + } getSortedKeys := func(keys []crypto.PublicKey) (encoded []string) { for _, key := range keys { encoded = append(encoded, hex.EncodeToString(key.Bytes())) @@ -260,3 +290,32 @@ func (g *Governance) Equal(other *Governance, checkState bool) bool { } return true } + +// RegisterConfigChange tells this governance instance to request some +// configuration change at some round. +// NOTE: you can't request config change for round 0, 1, they are genesis +// rounds. +// NOTE: this function should be called before running. +func (g *Governance) RegisterConfigChange( + round uint64, t StateChangeType, v interface{}) (err error) { + if t < StateChangeNumChains || t > StateChangeDKGSetSize { + return fmt.Errorf("state changes to register is not supported: %v", t) + } + if round < 2 { + return errors.New( + "attempt to register state change for genesis rounds") + } + g.lock.Lock() + defer g.lock.Unlock() + if round <= uint64(len(g.configs)) { + return errors.New( + "attempt to register state change for prepared rounds") + } + pendingChangesForRound, exists := g.pendingConfigChanges[round] + if !exists { + pendingChangesForRound = make(map[StateChangeType]interface{}) + g.pendingConfigChanges[round] = pendingChangesForRound + } + pendingChangesForRound[t] = v + return nil +} diff --git a/core/test/governance_test.go b/core/test/governance_test.go index 16de2a1..fe0dde7 100644 --- a/core/test/governance_test.go +++ b/core/test/governance_test.go @@ -59,6 +59,36 @@ func (s *GovernanceTestSuite) TestEqual() { req.False(g1.Equal(g4, true)) } +func (s *GovernanceTestSuite) TestRegisterChange() { + req := s.Require() + _, genesisNodes, err := NewKeys(20) + req.NoError(err) + g, err := NewGovernance(genesisNodes, 100*time.Millisecond) + req.NoError(err) + // Unable to register change for genesis round. + req.Error(g.RegisterConfigChange(0, StateChangeNumChains, uint32(32))) + // Make some round prepared. + g.CatchUpWithRound(4) + req.Equal(g.Configuration(4).NumChains, uint32(20)) + // Unable to register change for prepared round. + req.Error(g.RegisterConfigChange(4, StateChangeNumChains, uint32(32))) + // Unable to register change for next notified round. + req.Error(g.RegisterConfigChange(5, StateChangeNumChains, uint32(32))) + // It's ok to make some change when condition is met. + req.NoError(g.RegisterConfigChange(6, StateChangeNumChains, uint32(32))) + req.NoError(g.RegisterConfigChange(7, StateChangeNumChains, uint32(40))) + // In local mode, state for round 6 would be ready after notified with + // round 5. + g.NotifyRoundHeight(5, 0) + // In local mode, state for round 7 would be ready after notified with + // round 6. + g.NotifyRoundHeight(6, 0) + // Notify governance to take a snapshot for round 7's configuration. + g.NotifyRoundHeight(7, 0) + req.Equal(g.Configuration(6).NumChains, uint32(32)) + req.Equal(g.Configuration(7).NumChains, uint32(40)) +} + func TestGovernance(t *testing.T) { suite.Run(t, new(GovernanceTestSuite)) } diff --git a/core/test/state.go b/core/test/state.go index 34c38bc..51c7ab7 100644 --- a/core/test/state.go +++ b/core/test/state.go @@ -180,6 +180,14 @@ func NewState( } } +// SwitchToRemoteMode turn this State instance into remote mode: all changes +// are pending, and need to be packed/unpacked to apply. +func (s *State) SwitchToRemoteMode() { + s.lock.Lock() + defer s.lock.Unlock() + s.local = false +} + // Snapshot returns configration that could be snapshotted. func (s *State) Snapshot() (*types.Config, []crypto.PublicKey) { s.lock.RLock() @@ -575,6 +583,13 @@ func (s *State) Apply(reqsAsBytes []byte) (err error) { s.lock.Lock() defer s.lock.Unlock() for _, req := range reqs { + if err = s.isValidRequest(req); err != nil { + if err == ErrDuplicatedChange { + err = nil + continue + } + return + } if err = s.applyRequest(req); err != nil { return } |