-
Notifications
You must be signed in to change notification settings - Fork 175
/
qc_client.go
188 lines (159 loc) · 6.28 KB
/
qc_client.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
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
package epochs
import (
"context"
"encoding/hex"
"errors"
"fmt"
"time"
"github.com/onflow/cadence"
"github.com/onflow/flow-core-contracts/lib/go/templates"
"github.com/rs/zerolog"
sdk "github.com/onflow/flow-go-sdk"
sdkcrypto "github.com/onflow/flow-go-sdk/crypto"
"github.com/onflow/flow-go/network"
"github.com/onflow/flow-go/consensus/hotstuff/model"
hotstuffver "github.com/onflow/flow-go/consensus/hotstuff/verification"
"github.com/onflow/flow-go/model/flow"
"github.com/onflow/flow-go/module"
)
const (
// TransactionSubmissionTimeout is the time after which we return an error.
TransactionSubmissionTimeout = 5 * time.Minute
// TransactionStatusRetryTimeout is the time after which the status of a
// transaction is checked again
TransactionStatusRetryTimeout = 1 * time.Second
)
// QCContractClient is a client to the Quorum Certificate contract. Allows the client to
// functionality to submit a vote and check if collection node has voted already.
type QCContractClient struct {
BaseClient
nodeID flow.Identifier // flow identifier of the collection node
env templates.Environment
}
// NewQCContractClient returns a new client to the Quorum Certificate contract
func NewQCContractClient(
log zerolog.Logger,
flowClient module.SDKClientWrapper,
flowClientANID flow.Identifier,
nodeID flow.Identifier,
accountAddress string,
accountKeyIndex uint,
qcContractAddress string,
signer sdkcrypto.Signer,
) *QCContractClient {
log = log.With().
Str("component", "qc_contract_client").
Str("flow_client_an_id", flowClientANID.String()).
Logger()
base := NewBaseClient(log, flowClient, accountAddress, accountKeyIndex, signer)
// set QCContractAddress to the contract address given
env := templates.Environment{QuorumCertificateAddress: qcContractAddress}
return &QCContractClient{
BaseClient: *base,
nodeID: nodeID,
env: env,
}
}
// SubmitVote submits the given vote to the cluster QC aggregator smart
// contract. This function returns only once the transaction has been
// processed by the network. An error is returned if the transaction has
// failed and should be re-submitted.
// Error returns:
// - network.TransientError for any errors from the underlying client, if the retry period has been exceeded
// - errTransactionExpired if the transaction has expired
// - errTransactionReverted if the transaction execution reverted
// - generic error in case of unexpected critical failure
func (c *QCContractClient) SubmitVote(ctx context.Context, vote *model.Vote) error {
// time method was invoked
started := time.Now()
// add a timeout to the context
ctx, cancel := context.WithTimeout(ctx, TransactionSubmissionTimeout)
defer cancel()
// get account for given address and also validates AccountKeyIndex is valid
account, err := c.GetAccount(ctx)
if err != nil {
// we consider all errors from client network calls to be transient and non-critical
return network.NewTransientErrorf("could not get account: %w", err)
}
// get latest finalized block to execute transaction
latestBlock, err := c.FlowClient.GetLatestBlock(ctx, false)
if err != nil {
// we consider all errors from client network calls to be transient and non-critical
return network.NewTransientErrorf("could not get latest block from node: %w", err)
}
// attach submit vote transaction template and build transaction
seqNumber := account.Keys[int(c.AccountKeyIndex)].SequenceNumber
tx := sdk.NewTransaction().
SetScript(templates.GenerateSubmitVoteScript(c.env)).
SetGasLimit(9999).
SetReferenceBlockID(latestBlock.ID).
SetProposalKey(account.Address, int(c.AccountKeyIndex), seqNumber).
SetPayer(account.Address).
AddAuthorizer(account.Address)
// add signature to the transaction
sigDataHex, err := cadence.NewString(hex.EncodeToString(vote.SigData))
if err != nil {
return fmt.Errorf("could not convert vote sig data: %w", err)
}
err = tx.AddArgument(sigDataHex)
if err != nil {
return fmt.Errorf("could not add raw vote data to transaction: %w", err)
}
// add message to the transaction
voteMessage := hotstuffver.MakeVoteMessage(vote.View, vote.BlockID)
voteMessageHex, err := cadence.NewString(hex.EncodeToString(voteMessage))
if err != nil {
return fmt.Errorf("could not convert vote message: %w", err)
}
err = tx.AddArgument(voteMessageHex)
if err != nil {
return fmt.Errorf("could not add raw vote data to transaction: %w", err)
}
// sign envelope using account signer
err = tx.SignEnvelope(account.Address, int(c.AccountKeyIndex), c.Signer)
if err != nil {
return fmt.Errorf("could not sign transaction: %w", err)
}
// submit signed transaction to node
c.Log.Info().Str("tx_id", tx.ID().Hex()).Msg("sending SubmitResult transaction")
txID, err := c.SendTransaction(ctx, tx)
if err != nil {
// context expiring is not a critical failure, wrap as transient
if errors.Is(err, ctx.Err()) {
return network.NewTransientErrorf("failed to submit transaction: context done: %w", err)
}
return fmt.Errorf("failed to submit transaction: %w", err)
}
err = c.WaitForSealed(ctx, txID, started)
if err != nil {
// context expiring is not a critical failure, wrap as transient
if errors.Is(err, ctx.Err()) {
return network.NewTransientErrorf("failed to submit transaction: context done: %w", err)
}
return fmt.Errorf("failed to wait for transaction seal: %w", err)
}
return nil
}
// Voted returns true if we have successfully submitted a vote to the
// cluster QC aggregator smart contract for the current epoch.
// Error returns:
// - network.TransientError for any errors from the underlying Flow client
// - generic error in case of unexpected critical failures
func (c *QCContractClient) Voted(ctx context.Context) (bool, error) {
// execute script to read if voted
template := templates.GenerateGetNodeHasVotedScript(c.env)
ret, err := c.FlowClient.ExecuteScriptAtLatestBlock(ctx, template, []cadence.Value{cadence.String(c.nodeID.String())})
if err != nil {
// we consider all errors from client network calls to be transient and non-critical
return false, network.NewTransientErrorf("could not execute voted script: %w", err)
}
voted, ok := ret.(cadence.Bool)
if !ok {
return false, fmt.Errorf("unexpected cadence type (%T) returned from Voted script", ret)
}
// check if node has voted
if !voted {
return false, nil
}
return true, nil
}