4141from p2p import protocol
4242from p2p .constants import (
4343 COMPLETION_TIMEOUT ,
44+ MAX_REQUEST_ATTEMPTS ,
4445 REPLY_TIMEOUT ,
4546)
4647from p2p .p2p_proto import (
@@ -159,16 +160,42 @@ async def get_contract_code(self, block_hash: Hash32, address: Address) -> bytes
159160 :param address: which contract to look up
160161
161162 :return: bytecode of the contract, ``b''`` if no code is set
162- """
163- peer = cast (LESPeer , self .peer_pool .highest_td_peer )
164163
164+ :raise NoEligiblePeers: if no peers are available to fulfill the request
165+ :raise TimeoutError: if an individual request or the overall process times out
166+ """
165167 # get account for later verification, and
166168 # to confirm that our highest total difficulty peer has the info
167169 try :
168170 account = await self .get_account (block_hash , address )
169171 except HeaderNotFound as exc :
170172 raise NoEligiblePeers ("Our best peer does not have header %s" % block_hash ) from exc
171173
174+ code_hash = account .code_hash
175+
176+ for _ in range (MAX_REQUEST_ATTEMPTS ):
177+ peer = cast (LESPeer , self .peer_pool .highest_td_peer )
178+ try :
179+ return await self ._get_contract_code_from_peer (block_hash , address , peer , code_hash )
180+ except BadLESResponse as exc :
181+ self .logger .warn ("Disconnecting from peer, because: %s" , exc )
182+ await self .disconnect_peer (peer , DisconnectReason .subprotocol_error )
183+ # reattempt after removing this peer from our pool
184+
185+ raise TimeoutError ("Could not get contract code within %d attempts" % MAX_REQUEST_ATTEMPTS )
186+
187+ async def _get_contract_code_from_peer (
188+ self ,
189+ block_hash : Hash32 ,
190+ address : Address ,
191+ peer : LESPeer ,
192+ code_hash : Hash32 ) -> bytes :
193+ """
194+ A single attempt to get the contract code from the given peer
195+
196+ :raise BadLESResponse: if the peer replies with contract code that does not match the
197+ account's code hash
198+ """
172199 # request contract code
173200 request_id = gen_request_id ()
174201 peer .sub_proto .send_get_contract_code (block_hash , keccak (address ), request_id )
@@ -180,18 +207,20 @@ async def get_contract_code(self, block_hash: Hash32, address: Address) -> bytes
180207 bytecode = reply ['codes' ][0 ]
181208
182209 # validate bytecode against a proven account
183- if account . code_hash == keccak (bytecode ):
210+ if code_hash == keccak (bytecode ):
184211 return bytecode
185212 elif bytecode == b'' :
186213 # TODO disambiguate failure types here, and raise the appropriate exception
187214 # An (incorrectly) empty bytecode might indicate a bad-acting peer, or it might not
188215 raise NoEligiblePeers ("Our best peer incorrectly responded with an empty code value" )
189216 else :
190217 # a bad-acting peer sent an invalid non-empty bytecode
191- # disconnect from the peer
192- await self .disconnect_peer (peer , DisconnectReason .subprotocol_error )
193- # try again with another peer
194- return await self .get_contract_code (block_hash , address )
218+ raise BadLESResponse ("Peer %s sent code %s that did not match hash %s in account %s" % (
219+ peer ,
220+ encode_hex (bytecode ),
221+ encode_hex (code_hash ),
222+ encode_hex (address ),
223+ ))
195224
196225 async def _get_block_header_by_hash (self , peer : LESPeer , block_hash : Hash32 ) -> BlockHeader :
197226 self .logger .debug ("Fetching header %s from %s" , encode_hex (block_hash ), peer )
0 commit comments