Skip to content

Commit 00403b3

Browse files
committed
Is light server bytecode malicious or uninformed?
1 parent 4a99e72 commit 00403b3

File tree

2 files changed

+95
-10
lines changed

2 files changed

+95
-10
lines changed

p2p/lightchain.py

Lines changed: 95 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@
3939

4040
from p2p.exceptions import (
4141
BadLESResponse,
42+
NoConnectedPeers,
4243
NoEligiblePeers,
4344
)
4445
from p2p.cancel_token import CancelToken
4546
from p2p import protocol
4647
from 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:

p2p/service.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44
import logging
55
from typing import (
66
Any,
7-
Awaitable,
87
Callable,
98
List,
109
Optional,
11-
TypeVar,
1210
cast,
1311
)
1412

0 commit comments

Comments
 (0)