github.com/ChainSafe/chainbridge-core@v1.4.2/chains/evm/executor/voter.go (about)

     1  // Copyright 2021 ChainSafe Systems
     2  // SPDX-License-Identifier: LGPL-3.0-only
     3  
     4  package executor
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"math/big"
    10  	"math/rand"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/ChainSafe/chainbridge-core/chains/evm/calls"
    15  	"github.com/ChainSafe/chainbridge-core/chains/evm/calls/consts"
    16  	"github.com/ChainSafe/chainbridge-core/chains/evm/calls/transactor"
    17  
    18  	"github.com/ChainSafe/chainbridge-core/chains/evm/executor/proposal"
    19  	"github.com/ChainSafe/chainbridge-core/relayer/message"
    20  	"github.com/ethereum/go-ethereum/accounts/abi"
    21  	"github.com/ethereum/go-ethereum/common"
    22  	ethereumTypes "github.com/ethereum/go-ethereum/core/types"
    23  	"github.com/ethereum/go-ethereum/rpc"
    24  	"github.com/rs/zerolog/log"
    25  )
    26  
    27  const (
    28  	maxSimulateVoteChecks = 5
    29  	maxShouldVoteChecks   = 40
    30  	shouldVoteCheckPeriod = 15
    31  )
    32  
    33  var (
    34  	Sleep = time.Sleep
    35  )
    36  
    37  type ChainClient interface {
    38  	RelayerAddress() common.Address
    39  	CallContract(ctx context.Context, callArgs map[string]interface{}, blockNumber *big.Int) ([]byte, error)
    40  	SubscribePendingTransactions(ctx context.Context, ch chan<- common.Hash) (*rpc.ClientSubscription, error)
    41  	TransactionByHash(ctx context.Context, hash common.Hash) (tx *ethereumTypes.Transaction, isPending bool, err error)
    42  	calls.ContractCallerDispatcher
    43  }
    44  
    45  type MessageHandler interface {
    46  	HandleMessage(m *message.Message) (*proposal.Proposal, error)
    47  }
    48  
    49  type BridgeContract interface {
    50  	IsProposalVotedBy(by common.Address, p *proposal.Proposal) (bool, error)
    51  	VoteProposal(proposal *proposal.Proposal, opts transactor.TransactOptions) (*common.Hash, error)
    52  	SimulateVoteProposal(proposal *proposal.Proposal) error
    53  	ProposalStatus(p *proposal.Proposal) (message.ProposalStatus, error)
    54  	GetThreshold() (uint8, error)
    55  }
    56  
    57  type EVMVoter struct {
    58  	mh                   MessageHandler
    59  	client               ChainClient
    60  	bridgeContract       BridgeContract
    61  	pendingProposalVotes map[common.Hash]uint8
    62  }
    63  
    64  // NewVoterWithSubscription creates an instance of EVMVoter that votes for
    65  // proposals on chain.
    66  //
    67  // It is created with a pending proposal subscription that listens to
    68  // pending voteProposal transactions and avoids wasting gas on sending votes
    69  // for transactions that will fail.
    70  // Currently, officially supported only by Geth nodes.
    71  func NewVoterWithSubscription(mh MessageHandler, client ChainClient, bridgeContract BridgeContract) (*EVMVoter, error) {
    72  	voter := &EVMVoter{
    73  		mh:                   mh,
    74  		client:               client,
    75  		bridgeContract:       bridgeContract,
    76  		pendingProposalVotes: make(map[common.Hash]uint8),
    77  	}
    78  
    79  	ch := make(chan common.Hash)
    80  	_, err := client.SubscribePendingTransactions(context.TODO(), ch)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	go voter.trackProposalPendingVotes(ch)
    85  
    86  	return voter, nil
    87  }
    88  
    89  // NewVoter creates an instance of EVMVoter that votes for proposal on chain.
    90  //
    91  // It is created without pending proposal subscription and is a fallback
    92  // for nodes that don't support pending transaction subscription and will vote
    93  // on proposals that already satisfy threshold.
    94  func NewVoter(mh MessageHandler, client ChainClient, bridgeContract BridgeContract) *EVMVoter {
    95  	return &EVMVoter{
    96  		mh:                   mh,
    97  		client:               client,
    98  		bridgeContract:       bridgeContract,
    99  		pendingProposalVotes: make(map[common.Hash]uint8),
   100  	}
   101  }
   102  
   103  // Execute checks if relayer already voted and is threshold
   104  // satisfied and casts a vote if it isn't.
   105  func (v *EVMVoter) Execute(m *message.Message) error {
   106  	prop, err := v.mh.HandleMessage(m)
   107  	if err != nil {
   108  		return err
   109  	}
   110  
   111  	votedByTheRelayer, err := v.bridgeContract.IsProposalVotedBy(v.client.RelayerAddress(), prop)
   112  	if err != nil {
   113  		log.Error().Err(err).Msgf("Fetching is proposal %v voted by relayer failed", prop)
   114  		return err
   115  	}
   116  	if votedByTheRelayer {
   117  		return nil
   118  	}
   119  
   120  	shouldVote, err := v.shouldVoteForProposal(prop, 0)
   121  	if err != nil {
   122  		log.Error().Err(err).Msgf("Should vote for proposal %v failed", prop)
   123  		return err
   124  	}
   125  
   126  	if !shouldVote {
   127  		log.Debug().Msgf("Proposal %+v already satisfies threshold", prop)
   128  		return nil
   129  	}
   130  	err = v.repetitiveSimulateVote(prop, 0)
   131  	if err != nil {
   132  		log.Error().Err(err).Msgf("Simulating proposal %+v vote failed", prop)
   133  		return err
   134  	}
   135  
   136  	hash, err := v.bridgeContract.VoteProposal(prop, transactor.TransactOptions{Priority: prop.Metadata.Priority})
   137  	if err != nil {
   138  		log.Error().Err(err).Msgf("voting for proposal %+v failed", prop)
   139  		return fmt.Errorf("voting failed. Err: %w", err)
   140  	}
   141  
   142  	log.Debug().Str("hash", hash.String()).Uint64("nonce", prop.DepositNonce).Msgf("Voted")
   143  	return nil
   144  }
   145  
   146  // shouldVoteForProposal checks if proposal already has threshold with pending
   147  // proposal votes from other relayers.
   148  // Only works properly in conjuction with NewVoterWithSubscription as without a subscription
   149  // no pending txs would be received and pending vote count would be 0.
   150  func (v *EVMVoter) shouldVoteForProposal(prop *proposal.Proposal, tries int) (bool, error) {
   151  	propID := prop.GetID()
   152  	defer delete(v.pendingProposalVotes, propID)
   153  
   154  	// random delay to prevent all relayers checking for pending votes
   155  	// at the same time and all of them sending another tx
   156  	Sleep(time.Duration(rand.Intn(shouldVoteCheckPeriod)) * time.Second)
   157  
   158  	ps, err := v.bridgeContract.ProposalStatus(prop)
   159  	if err != nil {
   160  		return false, err
   161  	}
   162  
   163  	if ps.Status == message.ProposalStatusExecuted || ps.Status == message.ProposalStatusCanceled {
   164  		return false, nil
   165  	}
   166  
   167  	threshold, err := v.bridgeContract.GetThreshold()
   168  	if err != nil {
   169  		return false, err
   170  	}
   171  
   172  	if ps.YesVotesTotal+v.pendingProposalVotes[propID] >= threshold && tries < maxShouldVoteChecks {
   173  		// Wait until proposal status is finalized to prevent missing votes
   174  		// in case of dropped txs
   175  		tries++
   176  		return v.shouldVoteForProposal(prop, tries)
   177  	}
   178  
   179  	return true, nil
   180  }
   181  
   182  // repetitiveSimulateVote repeatedly tries(5 times) to simulate vore proposal call until it succeeds
   183  func (v *EVMVoter) repetitiveSimulateVote(prop *proposal.Proposal, tries int) error {
   184  	err := v.bridgeContract.SimulateVoteProposal(prop)
   185  	if err != nil {
   186  		if tries < maxSimulateVoteChecks {
   187  			tries++
   188  			return v.repetitiveSimulateVote(prop, tries)
   189  		}
   190  		return err
   191  	} else {
   192  		return nil
   193  	}
   194  }
   195  
   196  // trackProposalPendingVotes tracks pending voteProposal txs from
   197  // other relayers and increases count of pending votes in pendingProposalVotes map
   198  // by proposal unique id.
   199  func (v *EVMVoter) trackProposalPendingVotes(ch chan common.Hash) {
   200  	for msg := range ch {
   201  		txData, _, err := v.client.TransactionByHash(context.TODO(), msg)
   202  		if err != nil {
   203  			log.Error().Err(err)
   204  			continue
   205  		}
   206  
   207  		a, err := abi.JSON(strings.NewReader(consts.BridgeABI))
   208  		if err != nil {
   209  			log.Error().Err(err)
   210  			continue
   211  		}
   212  
   213  		if len(txData.Data()) < 4 {
   214  			continue
   215  		}
   216  
   217  		m, err := a.MethodById(txData.Data()[:4])
   218  		if err != nil {
   219  			continue
   220  		}
   221  
   222  		data, err := m.Inputs.UnpackValues(txData.Data()[4:])
   223  		if err != nil {
   224  			log.Error().Err(err)
   225  			continue
   226  		}
   227  
   228  		if m.Name == "voteProposal" {
   229  			source := data[0].(uint8)
   230  			depositNonce := data[1].(uint64)
   231  			prop := proposal.Proposal{
   232  				Source:       source,
   233  				DepositNonce: depositNonce,
   234  			}
   235  
   236  			go v.increaseProposalVoteCount(msg, prop.GetID())
   237  		}
   238  	}
   239  }
   240  
   241  // increaseProposalVoteCount increases pending proposal vote for target proposal
   242  // and decreases it when transaction is mined.
   243  func (v *EVMVoter) increaseProposalVoteCount(hash common.Hash, propID common.Hash) {
   244  	v.pendingProposalVotes[propID]++
   245  
   246  	_, err := v.client.WaitAndReturnTxReceipt(hash)
   247  	if err != nil {
   248  		log.Error().Err(err)
   249  	}
   250  
   251  	v.pendingProposalVotes[propID]--
   252  }