github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/epochs/qc_client.go (about) 1 package epochs 2 3 import ( 4 "context" 5 "encoding/hex" 6 "errors" 7 "fmt" 8 "time" 9 10 "github.com/onflow/cadence" 11 "github.com/onflow/flow-core-contracts/lib/go/templates" 12 "github.com/rs/zerolog" 13 14 sdk "github.com/onflow/flow-go-sdk" 15 sdkcrypto "github.com/onflow/flow-go-sdk/crypto" 16 "github.com/onflow/flow-go/network" 17 18 "github.com/onflow/flow-go/consensus/hotstuff/model" 19 hotstuffver "github.com/onflow/flow-go/consensus/hotstuff/verification" 20 "github.com/onflow/flow-go/model/flow" 21 "github.com/onflow/flow-go/module" 22 ) 23 24 const ( 25 26 // TransactionSubmissionTimeout is the time after which we return an error. 27 TransactionSubmissionTimeout = 5 * time.Minute 28 29 // TransactionStatusRetryTimeout is the time after which the status of a 30 // transaction is checked again 31 TransactionStatusRetryTimeout = 1 * time.Second 32 ) 33 34 // QCContractClient is a client to the Quorum Certificate contract. Allows the client to 35 // functionality to submit a vote and check if collection node has voted already. 36 type QCContractClient struct { 37 BaseClient 38 39 nodeID flow.Identifier // flow identifier of the collection node 40 env templates.Environment 41 } 42 43 // NewQCContractClient returns a new client to the Quorum Certificate contract 44 func NewQCContractClient( 45 log zerolog.Logger, 46 flowClient module.SDKClientWrapper, 47 flowClientANID flow.Identifier, 48 nodeID flow.Identifier, 49 accountAddress string, 50 accountKeyIndex uint, 51 qcContractAddress string, 52 signer sdkcrypto.Signer, 53 ) *QCContractClient { 54 55 log = log.With(). 56 Str("component", "qc_contract_client"). 57 Str("flow_client_an_id", flowClientANID.String()). 58 Logger() 59 base := NewBaseClient(log, flowClient, accountAddress, accountKeyIndex, signer) 60 61 // set QCContractAddress to the contract address given 62 env := templates.Environment{QuorumCertificateAddress: qcContractAddress} 63 64 return &QCContractClient{ 65 BaseClient: *base, 66 nodeID: nodeID, 67 env: env, 68 } 69 } 70 71 // SubmitVote submits the given vote to the cluster QC aggregator smart 72 // contract. This function returns only once the transaction has been 73 // processed by the network. An error is returned if the transaction has 74 // failed and should be re-submitted. 75 // Error returns: 76 // - network.TransientError for any errors from the underlying client, if the retry period has been exceeded 77 // - errTransactionExpired if the transaction has expired 78 // - errTransactionReverted if the transaction execution reverted 79 // - generic error in case of unexpected critical failure 80 func (c *QCContractClient) SubmitVote(ctx context.Context, vote *model.Vote) error { 81 82 // time method was invoked 83 started := time.Now() 84 85 // add a timeout to the context 86 ctx, cancel := context.WithTimeout(ctx, TransactionSubmissionTimeout) 87 defer cancel() 88 89 // get account for given address and also validates AccountKeyIndex is valid 90 account, err := c.GetAccount(ctx) 91 if err != nil { 92 // we consider all errors from client network calls to be transient and non-critical 93 return network.NewTransientErrorf("could not get account: %w", err) 94 } 95 96 // get latest finalized block to execute transaction 97 latestBlock, err := c.FlowClient.GetLatestBlock(ctx, false) 98 if err != nil { 99 // we consider all errors from client network calls to be transient and non-critical 100 return network.NewTransientErrorf("could not get latest block from node: %w", err) 101 } 102 103 // attach submit vote transaction template and build transaction 104 seqNumber := account.Keys[int(c.AccountKeyIndex)].SequenceNumber 105 tx := sdk.NewTransaction(). 106 SetScript(templates.GenerateSubmitVoteScript(c.env)). 107 SetComputeLimit(9999). 108 SetReferenceBlockID(latestBlock.ID). 109 SetProposalKey(account.Address, int(c.AccountKeyIndex), seqNumber). 110 SetPayer(account.Address). 111 AddAuthorizer(account.Address) 112 113 // add signature to the transaction 114 sigDataHex, err := cadence.NewString(hex.EncodeToString(vote.SigData)) 115 if err != nil { 116 return fmt.Errorf("could not convert vote sig data: %w", err) 117 } 118 err = tx.AddArgument(sigDataHex) 119 if err != nil { 120 return fmt.Errorf("could not add raw vote data to transaction: %w", err) 121 } 122 123 // add message to the transaction 124 voteMessage := hotstuffver.MakeVoteMessage(vote.View, vote.BlockID) 125 voteMessageHex, err := cadence.NewString(hex.EncodeToString(voteMessage)) 126 if err != nil { 127 return fmt.Errorf("could not convert vote message: %w", err) 128 } 129 err = tx.AddArgument(voteMessageHex) 130 if err != nil { 131 return fmt.Errorf("could not add raw vote data to transaction: %w", err) 132 } 133 134 // sign envelope using account signer 135 err = tx.SignEnvelope(account.Address, int(c.AccountKeyIndex), c.Signer) 136 if err != nil { 137 return fmt.Errorf("could not sign transaction: %w", err) 138 } 139 140 // submit signed transaction to node 141 c.Log.Info().Str("tx_id", tx.ID().Hex()).Msg("sending SubmitResult transaction") 142 txID, err := c.SendTransaction(ctx, tx) 143 if err != nil { 144 // context expiring is not a critical failure, wrap as transient 145 if errors.Is(err, ctx.Err()) { 146 return network.NewTransientErrorf("failed to submit transaction: context done: %w", err) 147 } 148 return fmt.Errorf("failed to submit transaction: %w", err) 149 } 150 151 err = c.WaitForSealed(ctx, txID, started) 152 if err != nil { 153 // context expiring is not a critical failure, wrap as transient 154 if errors.Is(err, ctx.Err()) { 155 return network.NewTransientErrorf("failed to submit transaction: context done: %w", err) 156 } 157 return fmt.Errorf("failed to wait for transaction seal: %w", err) 158 } 159 160 return nil 161 } 162 163 // Voted returns true if we have successfully submitted a vote to the 164 // cluster QC aggregator smart contract for the current epoch. 165 // Error returns: 166 // - network.TransientError for any errors from the underlying Flow client 167 // - generic error in case of unexpected critical failures 168 func (c *QCContractClient) Voted(ctx context.Context) (bool, error) { 169 170 // execute script to read if voted 171 template := templates.GenerateGetNodeHasVotedScript(c.env) 172 ret, err := c.FlowClient.ExecuteScriptAtLatestBlock(ctx, template, []cadence.Value{cadence.String(c.nodeID.String())}) 173 if err != nil { 174 // we consider all errors from client network calls to be transient and non-critical 175 return false, network.NewTransientErrorf("could not execute voted script: %w", err) 176 } 177 178 voted, ok := ret.(cadence.Bool) 179 if !ok { 180 return false, fmt.Errorf("unexpected cadence type (%T) returned from Voted script", ret) 181 } 182 183 // check if node has voted 184 if !voted { 185 return false, nil 186 } 187 return true, nil 188 }