// 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"
)
// ErrUnmatchedBlockHeightWithConfig is for invalid parameters for NewRoundEvent.
type ErrUnmatchedBlockHeightWithConfig struct {
round uint64
reset uint64
blockHeight uint64
}
func (e ErrUnmatchedBlockHeightWithConfig) Error() string {
return fmt.Sprintf("unsynced block height and cfg: 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
}
// NextRoundValidationHeight returns the height to check if the next round is
// ready.
func (e RoundEventParam) NextRoundValidationHeight() uint64 {
return e.BeginHeight + e.Config.RoundLength*9/10
}
// NextCRSProposingHeight returns the height to propose CRS for next round.
func (e RoundEventParam) NextCRSProposingHeight() uint64 {
return e.BeginHeight + e.Config.RoundLength/2
}
// NextDKGPreparationHeight returns the height to prepare DKG set for next
// round.
func (e RoundEventParam) NextDKGPreparationHeight() uint64 {
return e.BeginHeight + e.Config.RoundLength*2/3
}
// NextRoundHeight returns the height of the beginning of next round.
func (e RoundEventParam) NextRoundHeight() uint64 {
return e.BeginHeight + e.Config.RoundLength
}
// NextTouchNodeSetCacheHeight returns the height to touch the node set cache.
func (e RoundEventParam) NextTouchNodeSetCacheHeight() uint64 {
return e.BeginHeight + e.Config.RoundLength*9/10
}
// NextDKGResetHeight returns the height to reset DKG for next period.
func (e RoundEventParam) NextDKGResetHeight() uint64 {
return e.BeginHeight + e.Config.RoundLength*8/10
}
// NextDKGRegisterHeight returns the height to register DKG.
func (e RoundEventParam) NextDKGRegisterHeight() uint64 {
return e.BeginHeight + e.Config.RoundLength/2
}
// 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, ErrUnmatchedBlockHeightWithConfig{
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)
}
// TriggerInitEvent triggers event from the initial setting.
func (e *RoundEvent) TriggerInitEvent() {
e.lock.Lock()
defer e.lock.Unlock()
events := []RoundEventParam{RoundEventParam{
Round: e.lastTriggeredRound,
Reset: e.lastTriggeredResetCount,
BeginHeight: e.config.LastPeriodBeginHeight(),
CRS: GetCRSWithPanic(e.gov, e.lastTriggeredRound, e.logger),
Config: GetConfigWithPanic(e.gov, e.lastTriggeredRound, e.logger),
}}
for _, h := range e.handlers {
h(events)
}
}
// 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],
)
}()
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
}