diff options
Diffstat (limited to 'core/utils/round-event.go')
-rw-r--r-- | core/utils/round-event.go | 302 |
1 files changed, 302 insertions, 0 deletions
diff --git a/core/utils/round-event.go b/core/utils/round-event.go new file mode 100644 index 0000000..bab1d32 --- /dev/null +++ b/core/utils/round-event.go @@ -0,0 +1,302 @@ +// Copyright 2019 The dexon-consensus Authors +// This file is part of the dexon-consensus library. +// +// The dexon-consensus library is free software: you can redistribute it +// and/or modify it under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 of the License, +// or (at your option) any later version. +// +// The dexon-consensus library is distributed in the hope that it will be +// useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +// General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the dexon-consensus library. If not, see +// <http://www.gnu.org/licenses/>. + +package utils + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/dexon-foundation/dexon-consensus/common" + "github.com/dexon-foundation/dexon-consensus/core/types" + typesDKG "github.com/dexon-foundation/dexon-consensus/core/types/dkg" +) + +// ErrUnmatchedBlockHeightWithGov is for invalid parameters for NewRoundEvent. +type ErrUnmatchedBlockHeightWithGov struct { + round uint64 + reset uint64 + blockHeight uint64 +} + +func (e ErrUnmatchedBlockHeightWithGov) Error() string { + return fmt.Sprintf("unsynced block height and gov: round:%d reset:%d h:%d", + e.round, e.reset, e.blockHeight) +} + +// RoundEventParam defines the parameters passed to event handlers of +// RoundEvent. +type RoundEventParam struct { + // 'Round' of next checkpoint, might be identical to previous checkpoint. + Round uint64 + // the count of reset DKG for 'Round+1'. + Reset uint64 + // the begin block height of this event, the end block height of this event + // would be BeginHeight + config.RoundLength. + BeginHeight uint64 + // The configuration for 'Round'. + Config *types.Config + // The CRS for 'Round'. + CRS common.Hash +} + +// NextRoundCheckpoint returns the height to check if the next round is ready. +func (e RoundEventParam) NextRoundCheckpoint() uint64 { + return e.BeginHeight + e.Config.RoundLength*8/10 +} + +// roundEventFn defines the fingerprint of handlers of round events. +type roundEventFn func([]RoundEventParam) + +// governanceAccessor is a subset of core.Governance to break the dependency +// between core and utils package. +type governanceAccessor interface { + // Configuration returns the configuration at a given round. + // Return the genesis configuration if round == 0. + Configuration(round uint64) *types.Config + + // CRS returns the CRS for a given round. + // Return the genesis CRS if round == 0. + CRS(round uint64) common.Hash + + // DKGComplaints gets all the DKGComplaints of round. + DKGComplaints(round uint64) []*typesDKG.Complaint + + // DKGMasterPublicKeys gets all the DKGMasterPublicKey of round. + DKGMasterPublicKeys(round uint64) []*typesDKG.MasterPublicKey + + // IsDKGFinal checks if DKG is final. + IsDKGFinal(round uint64) bool + + // DKGResetCount returns the reset count for DKG of given round. + DKGResetCount(round uint64) uint64 +} + +// RoundEvent would be triggered when either: +// - the next DKG set setup is ready. +// - the next DKG set setup is failed, and previous DKG set already reset the +// CRS. +type RoundEvent struct { + gov governanceAccessor + logger common.Logger + lock sync.Mutex + handlers []roundEventFn + config RoundBasedConfig + lastTriggeredRound uint64 + lastTriggeredResetCount uint64 + roundShift uint64 + ctx context.Context + ctxCancel context.CancelFunc +} + +// NewRoundEvent creates an RoundEvent instance. +func NewRoundEvent(parentCtx context.Context, gov governanceAccessor, + logger common.Logger, initRound uint64, + initRoundBeginHeight, initBlockHeight uint64, + roundShift uint64) (*RoundEvent, error) { + // We need to generate valid ending block height of this round (taken + // DKG reset count into consideration). + e := &RoundEvent{ + gov: gov, + logger: logger, + lastTriggeredRound: initRound, + roundShift: roundShift, + } + e.ctx, e.ctxCancel = context.WithCancel(parentCtx) + e.config = RoundBasedConfig{} + e.config.SetupRoundBasedFields(initRound, GetConfigWithPanic( + gov, initRound, logger)) + e.config.SetRoundBeginHeight(initRoundBeginHeight) + // Make sure the DKG reset count in current governance can cover the initial + // block height. + resetCount := gov.DKGResetCount(initRound + 1) + remains := resetCount + for ; resetCount > 0 && !e.config.Contains(initBlockHeight); remains-- { + e.config.ExtendLength() + } + if !e.config.Contains(initBlockHeight) { + return nil, ErrUnmatchedBlockHeightWithGov{ + round: initRound, + reset: resetCount, + blockHeight: initBlockHeight, + } + } + e.lastTriggeredResetCount = resetCount - remains + return e, nil +} + +// Register a handler to be called when new round is confirmed or new DKG reset +// is detected. +func (e *RoundEvent) Register(h roundEventFn) { + e.lock.Lock() + defer e.lock.Unlock() + e.handlers = append(e.handlers, h) +} + +// ValidateNextRound validate if the DKG set for next round is ready to go or +// failed to setup, all registered handlers would be called once some decision +// is made on chain. +// +// This method would block until at least one event is triggered. Multiple +// trigger in one call is possible. +func (e *RoundEvent) ValidateNextRound(blockHeight uint64) { + // To make triggers continuous and sequential, the next validation should + // wait for previous one finishing. That's why I use mutex here directly. + var events []RoundEventParam + e.lock.Lock() + defer e.lock.Unlock() + e.logger.Info("ValidateNextRound", + "height", blockHeight, + "round", e.lastTriggeredRound, + "count", e.lastTriggeredResetCount) + defer func() { + if len(events) == 0 { + return + } + for _, h := range e.handlers { + // To make sure all handlers receive triggers sequentially, we can't + // raise go routines here. + h(events) + } + }() + startRound := e.lastTriggeredRound + for { + var ( + dkgFailed, triggered bool + param RoundEventParam + beginHeight = blockHeight + ) + for { + param, dkgFailed, triggered = e.check(beginHeight, startRound, + dkgFailed) + if !triggered { + break + } + events = append(events, param) + beginHeight = param.BeginHeight + } + if len(events) > 0 { + break + } + select { + case <-e.ctx.Done(): + return + case <-time.After(500 * time.Millisecond): + } + } +} + +func (e *RoundEvent) check(blockHeight, startRound uint64, lastDKGCheck bool) ( + param RoundEventParam, dkgFailed bool, triggered bool) { + defer func() { + if !triggered { + return + } + // A simple assertion to make sure we didn't pick the wrong round. + if e.config.RoundID() != e.lastTriggeredRound { + panic(fmt.Errorf("triggered round not matched: %d, %d", + e.config.RoundID(), e.lastTriggeredRound)) + } + param.Round = e.lastTriggeredRound + param.Reset = e.lastTriggeredResetCount + param.BeginHeight = e.config.LastPeriodBeginHeight() + param.CRS = GetCRSWithPanic(e.gov, e.lastTriggeredRound, e.logger) + param.Config = GetConfigWithPanic(e.gov, e.lastTriggeredRound, e.logger) + e.logger.Info("new RoundEvent triggered", + "round", e.lastTriggeredRound, + "reset", e.lastTriggeredResetCount, + "begin-height", e.config.LastPeriodBeginHeight(), + "crs", param.CRS.String()[:6], + ) + }() + // Make sure current last config covers the blockHeight. + if !e.config.Contains(blockHeight) { + panic(ErrUnmatchedBlockHeightWithGov{ + round: e.lastTriggeredRound, + reset: e.lastTriggeredResetCount, + blockHeight: blockHeight, + }) + } + nextRound := e.lastTriggeredRound + 1 + if nextRound >= startRound+e.roundShift { + // Avoid access configuration newer than last confirmed one over + // 'roundShift' rounds. Fullnode might crash if we access it before it + // knows. + return + } + nextCfg := GetConfigWithPanic(e.gov, nextRound, e.logger) + resetCount := e.gov.DKGResetCount(nextRound) + if resetCount > e.lastTriggeredResetCount { + e.lastTriggeredResetCount++ + e.config.ExtendLength() + triggered = true + return + } + if lastDKGCheck { + // We know that DKG already failed, now wait for the DKG set from + // previous round to reset DKG and don't have to reconstruct the + // group public key again. + dkgFailed = true + return + } + if nextRound >= dkgDelayRound { + if !e.gov.IsDKGFinal(nextRound) { + e.logger.Debug("DKG is not final, waiting for DKG reset", + "round", nextRound, + "reset", e.lastTriggeredResetCount) + return + } + if _, err := typesDKG.NewGroupPublicKey( + nextRound, + e.gov.DKGMasterPublicKeys(nextRound), + e.gov.DKGComplaints(nextRound), + GetDKGThreshold(nextCfg)); err != nil { + e.logger.Debug( + "group public key setup failed, waiting for DKG reset", + "round", nextRound, + "reset", e.lastTriggeredResetCount) + dkgFailed = true + return + } + } + // The DKG set for next round is well prepared. + e.lastTriggeredRound = nextRound + e.lastTriggeredResetCount = 0 + rCfg := RoundBasedConfig{} + rCfg.SetupRoundBasedFields(nextRound, nextCfg) + rCfg.AppendTo(e.config) + e.config = rCfg + triggered = true + return +} + +// Stop the event source and block until last trigger returns. +func (e *RoundEvent) Stop() { + e.ctxCancel() +} + +// LastPeriod returns block height related info of the last period, including +// begin height and round length. +func (e *RoundEvent) LastPeriod() (begin uint64, length uint64) { + e.lock.Lock() + defer e.lock.Unlock() + begin = e.config.LastPeriodBeginHeight() + length = e.config.RoundEndHeight() - e.config.LastPeriodBeginHeight() + return +} |