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  }