@@ -219,7 +219,7 @@ async def challenge_miner(self, miner: Miner):
219219 return (verified , reason , details )
220220
221221
222- async def challenge_subtensor (miner : Miner , challenge ):
222+ async def challenge_subtensor (miner : Miner , challenge , max_retries = 3 ):
223223 """
224224 Challenge the subtensor by requesting the value of a property of a specific neuron in a specific subnet at a certain block
225225 """
@@ -228,116 +228,129 @@ async def challenge_subtensor(miner: Miner, challenge):
228228 details = None
229229 process_time = None
230230
231- try :
232- # Get the details of the challenge
233- block_hash , params , value = challenge
234-
235- ws = None
231+ # Retry logic for geographic connectivity issues
232+ for attempt in range (max_retries ):
236233 try :
237- ws = await websockets .connect (f"ws://{ miner .ip } :9944" )
238-
239- except asyncio .TimeoutError as ex :
240- return (verified , "WebSocket connection timed out" , str (ex ), process_time )
241-
242- except (InvalidURI , OSError ) as ex :
243- return (
244- verified ,
245- f"Invalid WebSocket URI ws://{ miner .ip } :9944" ,
246- str (ex ),
247- process_time ,
234+ # Get the details of the challenge
235+ block_hash , params , value = challenge
236+
237+ # Add small delay between retries
238+ if attempt > 0 :
239+ await asyncio .sleep (attempt * 1.0 ) # 1s, 2s, 3s delays
240+ btul .logging .debug (f"[{ CHALLENGE_NAME } ][{ miner .uid } ] Retry attempt { attempt + 1 } /{ max_retries } " )
241+
242+ ws = None
243+ try :
244+ # Increase connection timeout for international connections
245+ ws = await websockets .connect (
246+ f"ws://{ miner .ip } :9944" ,
247+ ping_timeout = None , # Disable ping timeout
248+ ping_interval = None , # Disable ping
249+ close_timeout = 10 , # 10s close timeout
250+ open_timeout = 15 , # 15s connection timeout
251+ additional_headers = {
252+ "Connection" : "keep-alive" ,
253+ },
248254 )
249- except InvalidHandshake as ex :
250- return (verified , "WebSocket handshake failed" , str (ex ), process_time )
251-
252- except WebSocketException as ex :
253- return (verified , "Websocket exception" , str (ex ), process_time )
254-
255- # Set start time
256- start_time = time .time ()
257-
258- # Prepare data payload
259- data = json .dumps (
260- {
261- "id" : "state_call0" ,
262- "jsonrpc" : "2.0" ,
263- "method" : "state_call" ,
264- "params" : [
265- "NeuronInfoRuntimeApi_get_neuron_lite" ,
266- params ,
267- block_hash ,
268- ],
269- }
270- )
271255
272- try :
273- # Send request
274- await ws .send (data )
275-
276- except ConnectionClosed as ex :
277- return (
278- verified ,
279- "WebSocket closed before sending data" ,
280- str (ex ),
281- process_time ,
256+ except InvalidURI as ex :
257+ # Invalid URI - don't retry, fail immediately
258+ reason = f"Invalid WebSocket URI ws://{ miner .ip } :9944"
259+ details = str (ex )
260+ break
261+
262+ except (asyncio .TimeoutError , OSError , InvalidHandshake , WebSocketException ) as ex :
263+ # These are connection errors - let retry logic handle them
264+ raise ex
265+
266+
267+ # Set start time
268+ start_time = time .time ()
269+
270+ # Prepare data payload
271+ data = json .dumps (
272+ {
273+ "id" : "state_call0" ,
274+ "jsonrpc" : "2.0" ,
275+ "method" : "state_call" ,
276+ "params" : [
277+ "NeuronInfoRuntimeApi_get_neuron_lite" ,
278+ params ,
279+ block_hash ,
280+ ],
281+ }
282282 )
283283
284- try :
285- # Receive response
286- response = await ws .recv ()
287-
288- except asyncio .TimeoutError as ex :
289- return (verified , "WebSocket receive timed out" , str (ex ), process_time )
290-
291- except ConnectionClosed as ex :
292- return (
293- verified ,
294- "WebSocket closed before receiving response" ,
295- str (ex ),
296- process_time ,
297- )
284+ try :
285+ # Send request
286+ await ws .send (data )
298287
299- # Calculate process time
300- process_time = time .time () - start_time
288+ except ConnectionClosed as ex :
289+ # Connection error - let retry logic handle
290+ raise ex
301291
302- # Load the response
303- try :
304- response = json . loads ( response )
292+ try :
293+ # Receive response with timeout for international connections
294+ response = await asyncio . wait_for ( ws . recv (), timeout = 20.0 )
305295
306- except json .JSONDecodeError as ex :
307- return (verified , "Received malformed JSON response" , str (ex ), process_time )
296+ except (asyncio .TimeoutError , ConnectionClosed ) as ex :
297+ # Connection error - let retry logic handle
298+ raise ex
308299
309- if "error" in response :
310- return (
311- verified ,
312- f"Error in response: { response ['error' ].get ('message' , 'Unknown error' )} " ,
313- "" ,
314- process_time ,
315- )
300+ # Calculate process time
301+ process_time = time .time () - start_time
316302
317- if "result" not in response :
318- return (
319- verified ,
320- f"Response does not contain a 'result' field" ,
321- "" ,
322- process_time ,
323- )
303+ # Load the response
304+ try :
305+ response = json .loads (response )
324306
325- # Verify the challenge
326- verified = response ["result" ] == value
307+ except json .JSONDecodeError as ex :
308+ reason = "Received malformed JSON response"
309+ details = str (ex )
310+ break
327311
328- # Log total process time breakdown
329- total_time = time .time () - start_time
330- btul .logging .trace (
331- f"[{ CHALLENGE_NAME } ][{ miner .uid } ] Total challenge time: { total_time :.2f} s"
332- )
312+ if "error" in response :
313+ reason = f"Error in response: { response ['error' ].get ('message' , 'Unknown error' )} "
314+ details = ""
315+ break
333316
334- except Exception as ex :
335- reason = "Unexpected exception"
336- details = str (ex )
317+ if "result" not in response :
318+ reason = "Response does not contain a 'result' field"
319+ details = ""
320+ break
337321
338- finally :
339- if ws :
340- await ws .close ()
322+ # Verify the challenge
323+ verified = response ["result" ] == value
324+
325+ # Log total process time breakdown
326+ total_time = time .time () - start_time
327+ btul .logging .trace (
328+ f"[{ CHALLENGE_NAME } ][{ miner .uid } ] Total challenge time: { total_time :.2f} s"
329+ )
330+
331+ # Success - break retry loop
332+ break
333+
334+ except (ConnectionClosed , WebSocketException , asyncio .TimeoutError ) as ex :
335+ # These are retryable errors - continue to next attempt
336+ reason = f"Connection error on attempt { attempt + 1 } /{ max_retries } "
337+ details = str (ex )
338+ btul .logging .debug (f"[{ CHALLENGE_NAME } ][{ miner .uid } ] { reason } : { details } " )
339+
340+ if attempt == max_retries - 1 : # Last attempt
341+ reason = "WebSocket connection failed after retries"
342+ break
343+
344+ except Exception as ex :
345+ # Non-retryable error - break immediately
346+ reason = "Unexpected exception"
347+ details = str (ex )
348+ break
349+
350+ finally :
351+ if ws :
352+ await ws .close ()
353+ ws = None
341354
342355 return (verified , reason , details , process_time )
343356
0 commit comments