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 }