39
39
40
40
from p2p .exceptions import (
41
41
BadLESResponse ,
42
+ NoConnectedPeers ,
42
43
NoEligiblePeers ,
43
44
)
44
45
from p2p .cancel_token import CancelToken
45
46
from p2p import protocol
46
47
from p2p .constants import (
47
48
COMPLETION_TIMEOUT ,
49
+ MAX_REORG_DEPTH ,
48
50
MAX_REQUEST_ATTEMPTS ,
49
51
REPLY_TIMEOUT ,
50
52
)
@@ -118,8 +120,17 @@ def callback(r: protocol._DecodedMsgType) -> None:
118
120
@alru_cache (maxsize = 1024 , cache_exceptions = False )
119
121
@service_timeout (COMPLETION_TIMEOUT )
120
122
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
+ )
123
134
124
135
@alru_cache (maxsize = 1024 , cache_exceptions = False )
125
136
@service_timeout (COMPLETION_TIMEOUT )
@@ -164,7 +175,7 @@ async def _get_account_from_peer(
164
175
peer : LESPeer ) -> Account :
165
176
key = keccak (address )
166
177
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 )
168
179
try :
169
180
rlp_account = HexaryTrie .get_from_proof (header .state_root , key , proof )
170
181
except BadTrieProof as exc :
@@ -226,9 +237,9 @@ async def _get_contract_code_from_peer(
226
237
if code_hash == keccak (bytecode ):
227
238
return bytecode
228
239
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 " )
232
243
else :
233
244
# a bad-acting peer sent an invalid non-empty bytecode
234
245
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(
238
249
encode_hex (address ),
239
250
))
240
251
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
+ """
242
317
self .logger .debug ("Fetching header %s from %s" , encode_hex (block_hash ), peer )
243
318
request_id = gen_request_id ()
244
319
max_headers = 1
@@ -265,8 +340,20 @@ async def _get_proof(self,
265
340
return reply ['proof' ]
266
341
267
342
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
+ """
268
351
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
+
270
357
try :
271
358
return await make_request_to_peer (peer )
272
359
except BadLESResponse as exc :
0 commit comments