aboutsummaryrefslogblamecommitdiffstats
path: root/vendor/github.com/dexon-foundation/dexon-consensus/core/utils/round-event.go
blob: 0e70cf2502ce6d2a286dd8d453ce98f85f8ba011 (plain) (tree)






















                                                                               





                                                                             

                                                                                 




                          

                                                                                   


















                                                                                   




























                                                                              


                                                        




                                                            




                                                                      






                                                                        
























                                                                            


                                           

 














                                                                             












                                                                             
                                    






                                                                     
                               


                                                                             
                                                                







                                                          
                                                             





                                                                          



                                                                                    
                                                                             


                                                
                                                              










                                                                               

                                                      





                                               















                                                                                       



                                                                              

                                                                         




                                                                                 
                                           



                                                   

                                         







                                                                                            
             



                                                  
         
             

                                                                   

                             

                                               
         
              

 

                                                            





                                                                                  
                                                                               






                                                                                        
                                                         





                                                                         











                                                                                      
                                   


                                
                        


                                                                                 














                                                                                 
                                                                                       

                                                                   
                                          





                                                       
                           





















                                                                             
// 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"

    "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
}

// RoundEndHeight returns the round ending height of this round event.
func (e RoundEventParam) RoundEndHeight() uint64 {
    return e.BeginHeight + e.Config.RoundLength
}

func (e RoundEventParam) String() string {
    return fmt.Sprintf("roundEvtParam{Round:%d Reset:%d Height:%d}",
        e.Round,
        e.Reset,
        e.BeginHeight)
}

// 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

    // Get the begin height of a round.
    GetRoundHeight(round uint64) uint64
}

// RoundEventRetryHandlerGenerator generates a handler to common.Event, which
// would register itself to retry next round validation if round event is not
// triggered.
func RoundEventRetryHandlerGenerator(
    rEvt *RoundEvent, hEvt *common.Event) func(uint64) {
    var hEvtHandler func(uint64)
    hEvtHandler = func(h uint64) {
        if rEvt.ValidateNextRound(h) == 0 {
            // Retry until at least one round event is triggered.
            hEvt.RegisterHeight(h+1, hEvtHandler)
        }
    }
    return hEvtHandler
}

// 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
    dkgFailed               bool
    ctx                     context.Context
    ctxCancel               context.CancelFunc
}

// NewRoundEvent creates an RoundEvent instance.
func NewRoundEvent(parentCtx context.Context, gov governanceAccessor,
    logger common.Logger, initRound uint64,
    initBlockHeight uint64,
    roundShift uint64) (*RoundEvent, error) {
    // We need to generate valid ending block height of this round (taken
    // DKG reset count into consideration).
    initConfig := GetConfigWithPanic(gov, initRound, logger)
    e := &RoundEvent{
        gov:                gov,
        logger:             logger,
        lastTriggeredRound: initRound,
        roundShift:         roundShift,
    }
    e.ctx, e.ctxCancel = context.WithCancel(parentCtx)
    e.config = RoundBasedConfig{}
    e.config.SetupRoundBasedFields(initRound, initConfig)
    // TODO(jimmy): remove -1 after we match the height with fullnode.
    roundHeight := gov.GetRoundHeight(initRound)
    if initRound != 0 {
        roundHeight--
    }
    e.config.SetRoundBeginHeight(roundHeight)
    // Make sure the DKG reset count in current governance can cover the initial
    // block height.
    resetCount := gov.DKGResetCount(initRound + 1)
    remains := resetCount
    for ; remains > 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.
//
// The earlier registered handler has higher priority.
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.
//
// The count of triggered events would be returned.
func (e *RoundEvent) ValidateNextRound(blockHeight uint64) (count uint) {
    // 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.Trace("ValidateNextRound",
        "height", blockHeight,
        "round", e.lastTriggeredRound,
        "count", e.lastTriggeredResetCount)
    defer func() {
        count = uint(len(events))
        if count == 0 {
            return
        }
        for _, h := range e.handlers {
            // To make sure all handlers receive triggers sequentially, we can't
            // raise go routines here.
            h(events)
        }
    }()
    var (
        triggered   bool
        param       RoundEventParam
        beginHeight = blockHeight
        startRound  = e.lastTriggeredRound
    )
    for {
        param, triggered = e.check(beginHeight, startRound)
        if !triggered {
            break
        }
        events = append(events, param)
        beginHeight = param.BeginHeight
    }
    return
}

func (e *RoundEvent) check(blockHeight, startRound uint64) (
    param RoundEventParam, 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()
        e.dkgFailed = false
        triggered = true
        return
    }
    if e.dkgFailed {
        // 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.
        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)
            e.dkgFailed = true
            return
        }
    }
    // The DKG set for next round is well prepared.
    e.lastTriggeredRound = nextRound
    e.lastTriggeredResetCount = 0
    e.dkgFailed = false
    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
}