3939
4040from p2p .exceptions import (
4141 BadLESResponse ,
42+ NoConnectedPeers ,
4243 NoEligiblePeers ,
4344)
4445from p2p .cancel_token import CancelToken
4546from p2p import protocol
4647from p2p .constants import (
4748 COMPLETION_TIMEOUT ,
49+ MAX_REORG_DEPTH ,
4850 MAX_REQUEST_ATTEMPTS ,
4951 REPLY_TIMEOUT ,
5052)
@@ -118,8 +120,17 @@ def callback(r: protocol._DecodedMsgType) -> None:
118120 @alru_cache (maxsize = 1024 , cache_exceptions = False )
119121 @service_timeout (COMPLETION_TIMEOUT )
120122 async def get_block_header_by_hash (self , block_hash : Hash32 ) -> BlockHeader :
121- peer = cast (LESPeer , self .peer_pool .highest_td_peer )
122- return await self ._get_block_header_by_hash (peer , block_hash )
123+ """
124+ :param block_hash: hash of the header to retrieve
125+
126+ :return: header returned by peer
127+
128+ :raise NoEligiblePeers: if no peers are available to fulfill the request
129+ :raise TimeoutError: if an individual request or the overall process times out
130+ """
131+ return await self ._retry_on_bad_response (
132+ partial (self ._get_block_header_by_hash , block_hash )
133+ )
123134
124135 @alru_cache (maxsize = 1024 , cache_exceptions = False )
125136 @service_timeout (COMPLETION_TIMEOUT )
@@ -164,7 +175,7 @@ async def _get_account_from_peer(
164175 peer : LESPeer ) -> Account :
165176 key = keccak (address )
166177 proof = await self ._get_proof (peer , block_hash , account_key = b'' , key = key )
167- header = await self ._get_block_header_by_hash (peer , block_hash )
178+ header = await self ._get_block_header_by_hash (block_hash , peer )
168179 try :
169180 rlp_account = HexaryTrie .get_from_proof (header .state_root , key , proof )
170181 except BadTrieProof as exc :
@@ -226,9 +237,9 @@ async def _get_contract_code_from_peer(
226237 if code_hash == keccak (bytecode ):
227238 return bytecode
228239 elif bytecode == b'' :
229- # TODO disambiguate failure types here, and raise the appropriate exception
230- # An (incorrectly) empty bytecode might indicate a bad-acting peer, or it might not
231- raise NoEligiblePeers ( "Our best peer incorrectly responded with an empty code value " )
240+ await self . _raise_for_empty_code ( block_hash , address , code_hash , peer )
241+ # The following is added for mypy linting:
242+ raise RuntimeError ( "Unreachable, _raise_for_empty_code must raise its own exception " )
232243 else :
233244 # a bad-acting peer sent an invalid non-empty bytecode
234245 raise BadLESResponse ("Peer %s sent code %s that did not match hash %s in account %s" % (
@@ -238,7 +249,71 @@ async def _get_contract_code_from_peer(
238249 encode_hex (address ),
239250 ))
240251
241- async def _get_block_header_by_hash (self , peer : LESPeer , block_hash : Hash32 ) -> BlockHeader :
252+ async def _raise_for_empty_code (
253+ self ,
254+ block_hash : Hash32 ,
255+ address : Address ,
256+ code_hash : Hash32 ,
257+ peer : LESPeer ) -> None :
258+ """
259+ A peer might return b'' if it doesn't have the block at the requested header,
260+ or it might maliciously return b'' when the code is non-empty. This method tries to tell the
261+ difference.
262+
263+ This method MUST raise an exception, it's trying to determine the appropriate one.
264+
265+ :raise BadLESResponse: if peer seems to be maliciously responding with invalid empty code
266+ :raise NoEligiblePeers: if peer might simply not have the code available
267+ """
268+ try :
269+ header = await self ._get_block_header_by_hash (block_hash , peer )
270+ except HeaderNotFound :
271+ # We presume that the current peer is the best peer. Because
272+ # our best peer doesn't have the header we want, there are no eligible peers.
273+ raise NoEligiblePeers ("Our best peer does not have the header %s" % block_hash )
274+
275+ head_number = peer .head_info .block_number
276+ if head_number - header .block_number > MAX_REORG_DEPTH :
277+ # The peer claims to be far ahead of the header we requested
278+ if self .headerdb .get_canonical_block_hash (header .block_number ) == block_hash :
279+ # Our node believes that the header at the reference hash is canonical,
280+ # so treat the peer as malicious
281+ raise BadLESResponse (
282+ "Peer %s sent empty code that did not match hash %s in account %s" % (
283+ peer ,
284+ encode_hex (code_hash ),
285+ encode_hex (address ),
286+ )
287+ )
288+ else :
289+ # our header isn't canonical, so treat the empty response as missing data
290+ raise NoEligiblePeers (
291+ "Our best peer does not have the non-canonical header %s" % block_hash
292+ )
293+ elif head_number - header .block_number < 0 :
294+ # The peer claims to be behind the header we requested, but somehow served it to us.
295+ # Odd, it might be a race condition. Treat as if there are no eligible peers for now.
296+ raise NoEligiblePeers ("Our best peer's head does include header %s" % block_hash )
297+ else :
298+ # The peer is ahead of the current block header, but only by a bit. It might be on
299+ # an uncle, or we might be. So we can't tell the difference between missing and
300+ # malicious. We don't want to aggressively drop this peer, so treat the code as missing.
301+ raise NoEligiblePeers (
302+ "Peer %s claims to be ahead of %s, but returned empty code with hash %s. "
303+ "It is on number %d, maybe an uncle. Retry with an older block hash." % (
304+ peer ,
305+ header ,
306+ code_hash ,
307+ head_number ,
308+ )
309+ )
310+
311+ async def _get_block_header_by_hash (self , block_hash : Hash32 , peer : LESPeer ) -> BlockHeader :
312+ """
313+ A single attempt to get the block header from the given peer.
314+
315+ :raise BadLESResponse: if the peer replies with a header that has a different hash
316+ """
242317 self .logger .debug ("Fetching header %s from %s" , encode_hex (block_hash ), peer )
243318 request_id = gen_request_id ()
244319 max_headers = 1
@@ -265,8 +340,20 @@ async def _get_proof(self,
265340 return reply ['proof' ]
266341
267342 async def _retry_on_bad_response (self , make_request_to_peer : Callable [[LESPeer ], Any ]) -> Any :
343+ """
344+ Make a call to a peer. If it behaves badly, drop it and retry with a different peer.
345+
346+ :param make_request_to_peer: an abstract call to a peer that may raise a BadLESResponse
347+
348+ :raise NoEligiblePeers: if no peers are available to fulfill the request
349+ :raise TimeoutError: if an individual request or the overall process times out
350+ """
268351 for _ in range (MAX_REQUEST_ATTEMPTS ):
269- peer = cast (LESPeer , self .peer_pool .highest_td_peer )
352+ try :
353+ peer = cast (LESPeer , self .peer_pool .highest_td_peer )
354+ except NoConnectedPeers as exc :
355+ raise NoEligiblePeers () from exc
356+
270357 try :
271358 return await make_request_to_peer (peer )
272359 except BadLESResponse as exc :
0 commit comments