-
Notifications
You must be signed in to change notification settings - Fork 175
/
qc_voter.go
148 lines (126 loc) · 4.55 KB
/
qc_voter.go
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
package epochs
import (
"context"
"fmt"
"math/rand"
"time"
"github.com/rs/zerolog"
"github.com/onflow/flow-go/consensus/hotstuff"
hotmodel "github.com/onflow/flow-go/consensus/hotstuff/model"
"github.com/onflow/flow-go/model/flow"
"github.com/onflow/flow-go/module"
clusterstate "github.com/onflow/flow-go/state/cluster"
"github.com/onflow/flow-go/state/protocol"
)
// RootQCVoter is responsible for generating and submitting votes for the
// root quorum certificate of the upcoming epoch for this node's cluster.
type RootQCVoter struct {
log zerolog.Logger
me module.Local
signer hotstuff.Signer
state protocol.State
client module.QCContractClient // client to the QC aggregator smart contract
wait time.Duration // how long to sleep in between vote attempts
}
// NewRootQCVoter returns a new root QC voter, configured for a particular epoch.
func NewRootQCVoter(
log zerolog.Logger,
me module.Local,
signer hotstuff.Signer,
state protocol.State,
client module.QCContractClient,
) *RootQCVoter {
voter := &RootQCVoter{
log: log.With().Str("module", "root_qc_voter").Logger(),
me: me,
signer: signer,
state: state,
client: client,
wait: time.Second * 10,
}
return voter
}
// Vote handles the full procedure of generating a vote, submitting it to the
// epoch smart contract, and verifying submission. Returns an error only if
// there is a critical error that would make it impossible for the vote to be
// submitted. Otherwise, exits when the vote has been successfully submitted.
//
// It is safe to run multiple times within a single setup phase.
func (voter *RootQCVoter) Vote(ctx context.Context, epoch protocol.Epoch) error {
counter, err := epoch.Counter()
if err != nil {
return fmt.Errorf("could not get epoch counter: %w", err)
}
clusters, err := epoch.Clustering()
if err != nil {
return fmt.Errorf("could not get clustering: %w", err)
}
cluster, clusterIndex, ok := clusters.ByNodeID(voter.me.NodeID())
if !ok {
return fmt.Errorf("could not find self in clustering")
}
log := voter.log.With().
Uint64("epoch", counter).
Uint("cluster_index", clusterIndex).
Logger()
log.Info().Msg("preparing to generate vote for cluster root qc")
// create the canonical root block for our cluster
root := clusterstate.CanonicalRootBlock(counter, cluster)
// create a signable hotstuff model
signable := hotmodel.GenesisBlockFromFlow(root.Header)
vote, err := voter.signer.CreateVote(signable)
if err != nil {
return fmt.Errorf("could not create vote for cluster root qc: %w", err)
}
attempts := 0
for {
attempts++
log := log.With().Int("attempt", attempts).Logger()
// for all attempts after the first, wait before re-trying
if attempts > 1 {
wait := voter.getWaitInterval(attempts - 2) // -2 so that we wait the base interval in the first Sleep
log.Info().Msgf("waiting for %s before retry", wait.String())
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled: %w", ctx.Err())
case <-time.After(wait):
// proceed and re-submit vote
}
}
// check that we're still in the setup phase, if we're not we can't
// submit a vote anyway and must exit this process
phase, err := voter.state.Final().Phase()
if err != nil {
log.Error().Err(err).Msg("could not get current phase")
} else if phase != flow.EpochPhaseSetup {
return fmt.Errorf("could not submit vote - no longer in setup phase")
}
// check whether we've already voted, if we have we can exit early
voted, err := voter.client.Voted(ctx)
if err != nil {
log.Error().Err(err).Msg("could not check vote status")
continue
} else if voted {
log.Info().Msg("already voted - exiting QC vote process...")
return nil
}
// submit the vote - this call will block until the transaction has
// either succeeded or we are able to retry
log.Info().Msg("submitting vote...")
err = voter.client.SubmitVote(ctx, vote)
if err != nil {
log.Error().Err(err).Msg("could not submit vote - retrying...")
continue
}
log.Info().Msg("successfully submitted vote - exiting QC vote process...")
return nil
}
}
// getWaitInterval returns an interval to wait after the given number of attempts.
// The interval includes some jitter to avoid synchronization of requests from
// all collection nodes.
func (voter *RootQCVoter) getWaitInterval(attempts int) time.Duration {
base := voter.wait << attempts // base wait period on a geometric backoff
jitter := float64(base) * rand.Float64() * .1 // add 10% jitter to avoid synchronization across cluster
return time.Duration(base) + time.Duration(jitter)
}