41
41
from p2p import protocol
42
42
from p2p .constants import (
43
43
COMPLETION_TIMEOUT ,
44
+ MAX_REQUEST_ATTEMPTS ,
44
45
REPLY_TIMEOUT ,
45
46
)
46
47
from p2p .p2p_proto import (
@@ -159,16 +160,42 @@ async def get_contract_code(self, block_hash: Hash32, address: Address) -> bytes
159
160
:param address: which contract to look up
160
161
161
162
:return: bytecode of the contract, ``b''`` if no code is set
162
- """
163
- peer = cast (LESPeer , self .peer_pool .highest_td_peer )
164
163
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
+ """
165
167
# get account for later verification, and
166
168
# to confirm that our highest total difficulty peer has the info
167
169
try :
168
170
account = await self .get_account (block_hash , address )
169
171
except HeaderNotFound as exc :
170
172
raise NoEligiblePeers ("Our best peer does not have header %s" % block_hash ) from exc
171
173
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
+ """
172
199
# request contract code
173
200
request_id = gen_request_id ()
174
201
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
180
207
bytecode = reply ['codes' ][0 ]
181
208
182
209
# validate bytecode against a proven account
183
- if account . code_hash == keccak (bytecode ):
210
+ if code_hash == keccak (bytecode ):
184
211
return bytecode
185
212
elif bytecode == b'' :
186
213
# TODO disambiguate failure types here, and raise the appropriate exception
187
214
# An (incorrectly) empty bytecode might indicate a bad-acting peer, or it might not
188
215
raise NoEligiblePeers ("Our best peer incorrectly responded with an empty code value" )
189
216
else :
190
217
# 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
+ ))
195
224
196
225
async def _get_block_header_by_hash (self , peer : LESPeer , block_hash : Hash32 ) -> BlockHeader :
197
226
self .logger .debug ("Fetching header %s from %s" , encode_hex (block_hash ), peer )
0 commit comments