aboutsummaryrefslogtreecommitdiffstats
path: root/core/utils/round-event.go
blob: 1ce877ddad80a1ef92592973645ee345464ccab8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
// 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
}