Arpan Tripathi commited on
Commit
ed98ac4
·
unverified ·
1 Parent(s): 4758a24

retry inviting rejected miner UIDs in competition enrollment due to low alpha stake (#190)

Browse files
docs/competitions.md CHANGED
@@ -112,6 +112,11 @@ competition_start_time
112
  < competition_end_time
113
  ```
114
 
 
 
 
 
 
115
  ### Execution resources and safety
116
 
117
  | Field | Meaning |
 
112
  < competition_end_time
113
  ```
114
 
115
+ A miner below `minimum_alpha_stake` remains in the invitation queue and is
116
+ retried at `contender_ping_interval` until enrollment ends. If its stake reaches
117
+ the threshold in time, it can opt in and submit normally. The finalisation check
118
+ still rejects any pinned submission that is below the threshold at the freeze.
119
+
120
  ### Execution resources and safety
121
 
122
  | Field | Meaning |
tests/competition/test_enrollment.py CHANGED
@@ -271,17 +271,14 @@ class EnrollmentDispatcherTests(unittest.TestCase):
271
  len([call for call in forwarder.calls if call[0] == "invitation"]), 2
272
  )
273
 
274
- def test_ineligible_alpha_stake_is_reported_and_persisted(self) -> None:
275
  self.competition.manifest_json = self.manifest.model_copy(
276
  update={"minimum_alpha_stake": 10}
277
  ).normalized_json()
278
  endpoints = [
279
  CompetitionMinerEndpoint(
280
- 1, "participating-hotkey", "coldkey-1", object(), alpha_stake=10
281
- ),
282
- CompetitionMinerEndpoint(
283
- 2, "declining-hotkey", "coldkey-2", object(), alpha_stake=9
284
- ),
285
  ]
286
  forwarder = FakeForwarder()
287
  dispatcher = CompetitionEnrollmentDispatcher(
@@ -294,27 +291,20 @@ class EnrollmentDispatcherTests(unittest.TestCase):
294
 
295
  asyncio.run(dispatcher.run_once(self.competition, endpoints))
296
 
297
- self.assertCountEqual(
298
- forwarder.calls[:2],
299
- [
300
- ("invitation", "participating-hotkey"),
301
- ("invitation", "declining-hotkey"),
302
- ],
303
- )
304
- self.assertEqual(
305
- forwarder.calls[2], ("submission", "participating-hotkey")
306
- )
307
- rejected = self.repository.get_contender(
308
- self.manifest.competition_id, "declining-hotkey"
309
  )
310
- self.assertEqual(rejected.status, ContenderState.REJECTED.value)
311
- self.assertEqual(rejected.reason_code, "ALPHA_STAKE_BELOW_MINIMUM")
312
- self.assertIn("Observed alpha stake 9", rejected.reason_detail)
 
 
313
  rejection = next(
314
  response
315
  for response in forwarder.responses
316
  if isinstance(response, protocol.CompetitionInvitationProtocol)
317
- and response.axon.hotkey == "declining-hotkey"
318
  )
319
  self.assertEqual(
320
  rejection.eligibility_reason_code, "ALPHA_STAKE_BELOW_MINIMUM"
@@ -322,6 +312,43 @@ class EnrollmentDispatcherTests(unittest.TestCase):
322
  self.assertEqual(rejection.observed_alpha_stake, 9)
323
  self.assertEqual(rejection.minimum_alpha_stake, 10)
324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  def test_unavailable_alpha_stake_is_reported_and_persisted(self) -> None:
326
  self.competition.manifest_json = self.manifest.model_copy(
327
  update={"minimum_alpha_stake": 10}
 
271
  len([call for call in forwarder.calls if call[0] == "invitation"]), 2
272
  )
273
 
274
+ def test_below_minimum_alpha_stake_is_retried_until_miner_qualifies(self) -> None:
275
  self.competition.manifest_json = self.manifest.model_copy(
276
  update={"minimum_alpha_stake": 10}
277
  ).normalized_json()
278
  endpoints = [
279
  CompetitionMinerEndpoint(
280
+ 1, "participating-hotkey", "coldkey-1", object(), alpha_stake=9
281
+ )
 
 
 
282
  ]
283
  forwarder = FakeForwarder()
284
  dispatcher = CompetitionEnrollmentDispatcher(
 
291
 
292
  asyncio.run(dispatcher.run_once(self.competition, endpoints))
293
 
294
+ self.assertEqual(forwarder.calls, [("invitation", "participating-hotkey")])
295
+ contender = self.repository.get_contender(
296
+ self.manifest.competition_id, "participating-hotkey"
 
 
 
 
 
 
 
 
 
297
  )
298
+ self.assertEqual(contender.status, ContenderState.INVITED.value)
299
+ self.assertEqual(contender.reason_code, "ALPHA_STAKE_BELOW_MINIMUM")
300
+ self.assertIn("Observed alpha stake 9", contender.reason_detail)
301
+ self.assertEqual(contender.invitation_attempts, 1)
302
+ first_invited_at = contender.last_invited_at
303
  rejection = next(
304
  response
305
  for response in forwarder.responses
306
  if isinstance(response, protocol.CompetitionInvitationProtocol)
307
+ and response.axon.hotkey == "participating-hotkey"
308
  )
309
  self.assertEqual(
310
  rejection.eligibility_reason_code, "ALPHA_STAKE_BELOW_MINIMUM"
 
312
  self.assertEqual(rejection.observed_alpha_stake, 9)
313
  self.assertEqual(rejection.minimum_alpha_stake, 10)
314
 
315
+ # Rows written by older validators were terminally rejected. They are
316
+ # requeued on the next metagraph snapshot during enrollment.
317
+ with self.repository.engine.begin() as connection:
318
+ connection.exec_driver_sql(
319
+ "UPDATE contender_metadata SET status='REJECTED' "
320
+ "WHERE competition_id=? AND hotkey=?",
321
+ (self.manifest.competition_id, "participating-hotkey"),
322
+ )
323
+
324
+ self.clock.now += self.manifest.contender_ping_interval
325
+ asyncio.run(dispatcher.run_once(self.competition, endpoints))
326
+
327
+ contender = self.repository.get_contender(
328
+ self.manifest.competition_id, "participating-hotkey"
329
+ )
330
+ self.assertEqual(contender.status, ContenderState.INVITED.value)
331
+ self.assertEqual(contender.reason_code, "ALPHA_STAKE_BELOW_MINIMUM")
332
+ self.assertEqual(contender.invitation_attempts, 2)
333
+ self.assertNotEqual(contender.last_invited_at, first_invited_at)
334
+
335
+ self.clock.now += self.manifest.contender_ping_interval
336
+ qualified_endpoints = [
337
+ CompetitionMinerEndpoint(
338
+ 1, "participating-hotkey", "coldkey-1", object(), alpha_stake=10
339
+ )
340
+ ]
341
+ asyncio.run(dispatcher.run_once(self.competition, qualified_endpoints))
342
+
343
+ contender = self.repository.get_contender(
344
+ self.manifest.competition_id, "participating-hotkey"
345
+ )
346
+ self.assertEqual(contender.status, ContenderState.PARTICIPATING.value)
347
+ self.assertIsNone(contender.reason_code)
348
+ self.assertIsNone(contender.reason_detail)
349
+ self.assertEqual(contender.invitation_attempts, 3)
350
+ self.assertIn(("submission", "participating-hotkey"), forwarder.calls)
351
+
352
  def test_unavailable_alpha_stake_is_reported_and_persisted(self) -> None:
353
  self.competition.manifest_json = self.manifest.model_copy(
354
  update={"minimum_alpha_stake": 10}
vidaio_subnet_core/competition/enrollment.py CHANGED
@@ -254,6 +254,13 @@ class CompetitionEnrollmentDispatcher:
254
  "ENROLLMENT_WINDOW_CLOSED",
255
  "invitation response arrived after contender finalisation",
256
  )
 
 
 
 
 
 
 
257
  participating = invitation.participating and eligibility_reason_code is None
258
  await asyncio.to_thread(
259
  self.repository.record_invitation_response,
 
254
  "ENROLLMENT_WINDOW_CLOSED",
255
  "invitation response arrived after contender finalisation",
256
  )
257
+ if eligibility_reason_code == "ALPHA_STAKE_BELOW_MINIMUM":
258
+ # Match retryable transport failures such as CONTENDER_NOT_FOUND:
259
+ # record the reason without moving the row out of INVITED.
260
+ raise EnrollmentResponseError(
261
+ eligibility_reason_code,
262
+ eligibility_reason_detail or eligibility_reason_code,
263
+ )
264
  participating = invitation.participating and eligibility_reason_code is None
265
  await asyncio.to_thread(
266
  self.repository.record_invitation_response,
vidaio_subnet_core/competition/repository.py CHANGED
@@ -2776,6 +2776,17 @@ class CompetitionRepository:
2776
  row.uid_snapshot = uid_snapshot
2777
  row.coldkey_snapshot = coldkey_snapshot
2778
  row.updated_at = now_text
 
 
 
 
 
 
 
 
 
 
 
2779
  return created
2780
 
2781
  def reject_ineligible_contenders(
 
2776
  row.uid_snapshot = uid_snapshot
2777
  row.coldkey_snapshot = coldkey_snapshot
2778
  row.updated_at = now_text
2779
+ elif (
2780
+ row.status == ContenderState.REJECTED.value
2781
+ and row.reason_code == "ALPHA_STAKE_BELOW_MINIMUM"
2782
+ and row.pinned_commit_sha is None
2783
+ ):
2784
+ # Requeue invitation-time rejections from older validators.
2785
+ # Pinned finalisation rejections must remain closed.
2786
+ row.status = ContenderState.INVITED.value
2787
+ row.uid_snapshot = uid_snapshot
2788
+ row.coldkey_snapshot = coldkey_snapshot
2789
+ row.updated_at = now_text
2790
  return created
2791
 
2792
  def reject_ineligible_contenders(