File size: 75,301 Bytes
bedb966
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
"""Validator loop β€” manifest β†’ eval β†’ KOTH decision β†’ weights.

The validator never trains. Each round it:

1. Reads the current :class:`TrainingManifest` from the owner dataset repo and
   verifies its signature + that king and challenger share the contract digest
   (the controlled-experiment guarantee).
2. Pulls the king's and challenger's trained checkpoints and scores both on the
   *same* held-out eval windows.
3. Runs the paired-bootstrap KOTH verdict and folds it into the sticky
   champion state (``dethrone_cp`` consecutive wins to take the throne;
   ``dethrone_cp = 1`` makes it single-round).
4. Sets weights: equal share across the current king plus up to
   ``[scoring] reward_prior_kings`` registered prior kings (teutonic-style),
   collapsing to winner-take-all when ``reward_prior_kings = 0``.

The pure orchestration in :meth:`ValidatorRunner.process_round` is testable by
injecting ``evaluate_fn`` and ``windows``; HF + torch + chain are isolated
behind the defaults.
"""

from __future__ import annotations

import contextlib
import hashlib
import json
import logging
from collections.abc import Callable
from dataclasses import dataclass, field, replace
from datetime import UTC
from pathlib import Path
from typing import TYPE_CHECKING

from ..eval.koth import RoundResult, evaluate_round
from ..eval.scoring import WindowScore
from ..eval.window import EvalWindow
from ..shared.config import ChainConfig
from ..shared.manifest import (
    TrainedEntry,
    TrainingManifest,
    contract_digest,
    parse_trained_pointer,
    verify_signature,
)
from ..shared.receipt import (
    EntryScores,
    EvalContext,
    Participant,
    RoundReceipt,
    VerdictRecord,
    WindowScoreRecord,
    build_receipt,
)
from . import state as state_mod
from .state import ChampionState, StateTransition

if TYPE_CHECKING:
    from ..trainer.remote import RemoteHost
    from .cascade import CascadeController

log = logging.getLogger("cascade.validator")

# Resolve a trained entry to its per-window scores on the eval set.
EvaluateFn = Callable[[TrainedEntry, list[EvalWindow]], list[WindowScore]]
# Resolve a trained entry to its gift-eval ratio rows for the public-benchmark
# gate: ``{"status", "rows", "revision"}`` (see ``eval.benchmarks.run_gift_rows``)
# or ``None`` when the sidecar produced nothing.
GiftRowsFn = Callable[[TrainedEntry], dict | None]


def participants_from_commitments(commitments: list, cutoff_block: int,
                                  floor_block: int = 0) -> tuple[Participant, ...]:
    """The round's eligible participant set, for the public receipt.

    Mirrors the trainer's eligibility rule (``trainer.loop.resolve_commitments``):
    parseable generator pointers revealed STRICTLY BEFORE the epoch boundary and
    at/after the go-live ``floor_block``, latest commit per hotkey among the
    eligible ones β€” but keeps ``commit_block`` so an auditor can re-check every
    entrant against the cutoff. Sorted by UID for a deterministic receipt body.

    ``commitments`` should carry each hotkey's FULL reveal history
    (``poll_commitments(include_history=True)``): with only the latest reveal, a
    miner who re-committed after the boundary vanishes from the record even
    though their eligible pre-cutoff entry fielded the round.
    """
    from ..interface.validation import parse_commit

    best: dict[str, Participant] = {}
    for c in commitments:
        if floor_block and c.commit_block < floor_block:
            continue
        if c.commit_block >= cutoff_block:
            continue
        parsed = parse_commit(c.payload)
        if parsed is None:
            continue
        prev = best.get(c.hotkey)
        if prev is None or c.commit_block >= prev.commit_block:
            best[c.hotkey] = Participant(
                hotkey=c.hotkey, uid=c.uid, gen_ref=parsed.ref, commit_block=c.commit_block
            )
    return tuple(sorted(best.values(), key=lambda p: p.uid))


@dataclass(frozen=True)
class RoundOutcome:
    result: RoundResult
    transition: StateTransition
    # Every per-window score that fed the verdict, one record per evaluated
    # (role, size) entry in evaluation order β€” threaded out so the live loop can
    # publish them in the round's signed public receipt (cascade.shared.receipt).
    entry_scores: tuple[EntryScores, ...] = ()
    # The king's tenure AT decision time (it set the margin); recorded in the
    # receipt so an auditor can recompute margin_for_tenure without validator state.
    king_tenure_rounds: int = 0


# How long the live loop keeps re-trying a round whose eval-pool index cannot
# be READ at the pin gate (auth/network/5xx β€” not absence) before rejecting it
# for real. Sized for observed Hippius blips (seconds-to-minutes) with a wide
# margin β€” ~15 polls at the default 120s manifest cadence. A persistent failure
# (bad credentials, dead endpoint) still ends in the loud reject receipt, just
# this much later.
POOL_PIN_READ_GRACE_SECONDS = 1800.0


@dataclass
class ValidatorRunner:
    cfg: ChainConfig
    state: ChampionState = field(default_factory=ChampionState)
    evaluate_fn: EvaluateFn | None = None     # injected in tests; defaults to registry+torch
    gift_rows_fn: GiftRowsFn | None = None    # injected in tests; defaults to the sidecar bridge
    cache_dir: Path | None = None
    device: str = "cpu"
    # Optional resolver for the GPU eval-offload pod, called AT EACH offloaded
    # eval (see cascade.validator.eval_offload.make_eval_host_fn): the pod is
    # elastic β€” rented per round by the provisioner, torn down on the receipt β€”
    # so it must be re-resolved lazily, not captured at startup. ``None`` (or a
    # call returning ``None``) β‡’ that eval runs on ``device`` locally. The
    # wallet and every consensus decision stay on this box either way; the pod
    # is never used for the private-pool duel.
    eval_host_fn: Callable[[], RemoteHost | None] | None = None
    verify_signatures: bool = True            # gate manifests on the trainer-hotkey signature
    # Cascade β€” king-reign promotion (see cascade.validator.cascade). When wired,
    # the reign clock is reset on each dethrone, every reigning-king checkpoint is
    # scored (GIFT-Eval + TIME) and logged, and once per round the clock is checked;
    # a fired Cascade installs the promoted warm-start init and re-crowns the
    # same king (DEC-CA-0004). None β‡’ Cascade is disabled (pure KOTH).
    cascade: CascadeController | None = None
    # Block of the last successful (or attempted re-assert) weight-set; drives
    # the between-rounds freshness push in _maybe_reassert_weights. None β‡’
    # never set this process, so the first live-loop tick re-asserts
    # immediately (a restart is also a manual "refresh last_update now").
    _last_weight_block: int | None = None
    # First-failure clock (time.monotonic) per round for an UNREADABLE eval-pool
    # index at the pin gate. Within POOL_PIN_READ_GRACE_SECONDS the round is
    # re-tried on the next manifest poll (no latch, no reject receipt); once the
    # grace expires the reject goes through, loudly. In-memory on purpose: a
    # restart merely restarts the grace clock, which is harmless.
    _pin_read_first_failure: dict[str, float] = field(default_factory=dict, repr=False)

    # ── manifest gating ─────────────────────────────────────────────────────

    def check_manifest(self, manifest: TrainingManifest) -> str | None:
        """Return a rejection reason string, or None if the manifest is usable.

        Enforces (1) the trainer-hotkey signature, (2) the contract-digest match
        (king and challenger trained under the same terms), and (3) that the
        manifest targets our configured base architecture and eval dataset.
        """
        if self.verify_signatures and not verify_signature(manifest, self.cfg.manifest.trainer_hotkey):
            return "signature_invalid"
        want_contract = contract_digest(self.cfg.training)
        if manifest.contract_digest != want_contract:
            return f"contract_digest_mismatch: {manifest.contract_digest} != {want_contract}"
        if manifest.base_arch_digest != self.cfg.training.base_arch_digest:
            return "base_arch_digest_mismatch"
        if manifest.eval_dataset != self.cfg.eval.eval_dataset:
            return "eval_dataset_mismatch"
        gpu_reason = self._check_gpu(manifest)
        if gpu_reason is not None:
            return gpu_reason
        ws_reason = self._check_warm_start(manifest)
        if ws_reason is not None:
            return ws_reason
        return None

    def _check_warm_start(self, manifest: TrainingManifest) -> str | None:
        """Warm-start pin gate (Cascade, DEC-CA-0005): the manifest's signed
        ``warm_start_ckpt`` must equal the init THIS validator's deterministic
        promotion installed (its own ``warm_start_init_path`` file; "" before any
        promotion). Every validator computes the same promotion (block-anchored
        clock + trainer-signed bench scores), so agreement is fleet-wide. A
        mismatch β€” trainer trained from random when a promotion is live, or from
        a stale/foreign init β€” rejects the round rather than silently scoring
        runs trained off-baseline; the trainer re-syncs by the next round. Only
        enforced when Cascade is wired (off β‡’ pure KOTH, field ignored)."""
        if self.cascade is None:
            return None
        expected = ""
        p = Path(self.cfg.validator.warm_start_init_path)
        if p.is_file():
            try:
                expected = str(json.loads(p.read_text(encoding="utf-8")).get("checkpoint_id") or "")
            except Exception as e:  # noqa: BLE001 β€” unreadable pin must fail LOUD, not open
                return f"warm_start_state_unreadable: {p}: {e}"
        if manifest.warm_start_ckpt != expected:
            return (f"warm_start_mismatch: manifest trained from "
                    f"{manifest.warm_start_ckpt or '<random init>'!r}, this validator "
                    f"expects {expected or '<random init>'!r}")
        return None

    @staticmethod
    def check_pool_pin(
        manifest: TrainingManifest, window_source: object, *, block: int | None
    ) -> str | None:
        """Verify this round's eval pool against the trainer-signed pin.

        A pinned manifest carries the ``(key, sha256)`` of the snapshot the
        trainer screened on; the validator's own deterministic selection (same
        epoch block, same rule) must resolve to the identical pair. The pin is
        inside the signed body, so pool integrity descends from the trainer
        signature rather than the unsigned ``pool/index.json`` β€” a poisoned
        index or tampered tar surfaces here as a loud reject instead of scoring
        on attacker-chosen data. Unpinned manifests (older trainers) keep the
        legacy index-trust behaviour. Returns a reject reason or ``None``.

        Raises :class:`~cascade.shared.hippius.StorageError` when the pool
        index could not be READ (auth/network/5xx) β€” that is a transient, not a
        verdict, and the caller decides retry-vs-reject (the live loop retries
        within :data:`POOL_PIN_READ_GRACE_SECONDS`). Every other failure is a
        reject reason.
        """
        if not (manifest.eval_pool_key and manifest.eval_pool_sha256):
            return None
        prov_fn = getattr(window_source, "provenance_for_round", None)
        if prov_fn is None:
            return ("pool_pin_unverifiable: manifest pins the eval pool but this "
                    "validator's pool source reports no provenance")
        try:
            key, sha = prov_fn(int(manifest.round_id), block=block)
        except Exception as e:  # noqa: BLE001 β€” an unverifiable pin must reject, not crash
            from ..shared.hippius import StorageError

            if isinstance(e, StorageError):
                raise  # read failure, not a verdict β€” caller retries (see above)
            return f"pool_pin_unverifiable: provenance lookup failed: {e}"
        if not key or not sha:
            return ("pool_pin_unverifiable: manifest pins the eval pool but this "
                    "validator resolved no snapshot for the round")
        if (key, sha) != (manifest.eval_pool_key, manifest.eval_pool_sha256):
            return (f"pool_pin_mismatch: manifest signed {manifest.eval_pool_key}@"
                    f"{manifest.eval_pool_sha256[:12]}…, this validator resolved "
                    f"{key}@{sha[:12]}…")
        return None

    def _pool_pin_read_failed(
        self, round_id: str, err: Exception, *, now: float | None = None
    ) -> str | None:
        """Grace bookkeeping for an UNREADABLE pool index at the pin gate.

        Returns ``None`` while ``round_id``'s read failures span less than
        :data:`POOL_PIN_READ_GRACE_SECONDS` β€” the caller then skips the cycle
        with no latch and no receipt, so the next manifest poll re-attempts the
        round from scratch. Once the grace expires, returns the terminal reject
        reason (and forgets the round, so a later re-publish starts a fresh
        grace window).
        """
        import time

        now = time.monotonic() if now is None else now
        first = self._pin_read_first_failure.setdefault(str(round_id), now)
        waited = now - first
        if waited < POOL_PIN_READ_GRACE_SECONDS:
            log.warning(
                "pool index unreadable at pin gate for round=%s (%s); retrying "
                "next poll (%.0fs into %.0fs grace, no reject latched)",
                round_id, err, waited, POOL_PIN_READ_GRACE_SECONDS,
            )
            return None
        self._pin_read_first_failure.pop(str(round_id), None)
        return (
            f"pool_pin_unverifiable: provenance lookup failed persistently "
            f"({waited:.0f}s > {POOL_PIN_READ_GRACE_SECONDS:.0f}s grace): {err}"
        )

    def _check_gpu(self, manifest: TrainingManifest) -> str | None:
        """Matched-hardware gate for byte-exact re-derivation.

        If ``[training] expected_gpu`` is pinned, every entry must report that GPU.
        Otherwise require only that king and challenger ran the same GPU (when both
        report one) β€” equal compute is already guaranteed by the token budget, but
        a byte-exact audit needs the comparison run on one SKU.
        """
        pinned = self.cfg.training.expected_gpu
        gpus = {e.gpu_name for e in manifest.entries if e.gpu_name}
        if pinned:
            bad = sorted(g for g in gpus if g != pinned)
            if bad or any(not e.gpu_name for e in manifest.entries):
                return f"gpu_mismatch: expected {pinned!r}, manifest has {sorted(gpus)!r}"
        elif len(gpus) > 1:
            return f"gpu_mismatch: king/challenger on different GPUs {sorted(gpus)!r}"
        return None

    # ── per-round decision ──────────────────────────────────────────────────

    def _fetch_checkpoint_dir(self, entry: TrainedEntry) -> Path:
        """Fetch a trained checkpoint from the Hippius Hub registry to a local
        dir and return it. The OCI digest in the ref pins the bytes, so the
        fetch is self-verifying; repeated fetches of the same ref land in the
        same digest-named dir (cheap to reuse)."""
        from ..shared.hippius import HubConfig, HubRef, fetch_from_hub

        ref = parse_trained_pointer(entry.trained_pointer)
        if ref is None:
            raise ValueError(f"malformed trained_pointer: {entry.trained_pointer!r}")
        hub = HubConfig.from_storage(self.cfg.storage)
        dest = Path(self.cache_dir or "./_eval_ckpts") / HubRef.parse(ref).digest.replace(":", "-")
        fetch_from_hub(ref, dest, hub)
        return dest

    def _evaluate(self, entry: TrainedEntry, windows: list[EvalWindow]) -> list[WindowScore]:
        if self.evaluate_fn is not None:
            return self.evaluate_fn(entry, windows)
        # Default path: fetch the checkpoint from the Hippius Hub registry and
        # score it (registry + torch).
        from .evaluator import evaluate_checkpoint

        dest = self._fetch_checkpoint_dir(entry)
        return evaluate_checkpoint(
            dest, windows, num_samples=self.cfg.eval.num_samples, device=self.device
        )

    # ── public-benchmark no-regression gate ─────────────────────────────────

    def _eval_host(self) -> RemoteHost | None:
        """The offload pod for THIS eval β€” resolved fresh every time so an
        elastic pod (rented per round manifest, torn down on the receipt)
        appears and disappears without a validator restart."""
        return self.eval_host_fn() if self.eval_host_fn is not None else None

    def _gift_rows(self, entry: TrainedEntry) -> dict | None:
        """Gift-eval ratio rows for one entry β€” injected in tests, else the
        sidecar bridge on the fetched checkpoint dir."""
        if self.gift_rows_fn is not None:
            return self.gift_rows_fn(entry)

        ec = self.cfg.eval
        dest = self._fetch_checkpoint_dir(entry)
        num_samples = ec.gift_gate_num_samples or ec.num_samples
        eval_host = self._eval_host()
        if eval_host is not None:
            # Offload the (heavy, paired) gift-eval to the GPU pod; the paired
            # bootstrap and every consensus decision stay on this box.
            from .eval_offload import gift_rows_via_host

            return gift_rows_via_host(
                eval_host, dest,
                datasets=ec.gift_gate_datasets,
                num_samples=num_samples,
                data_dir=(ec.gift_gate_data_dir or None),
                device="cuda",
                timeout_s=ec.gift_gate_timeout_s,
            )
        from ..eval.benchmarks import run_gift_rows

        return run_gift_rows(
            dest,
            project_dir=ec.benchmark_project_dir,
            datasets=ec.gift_gate_datasets,
            num_samples=num_samples,
            device=self.device,
            data_dir=(ec.gift_gate_data_dir or None),
            timeout_s=ec.gift_gate_timeout_s,
        )

    def _run_gift_gate(
        self,
        result: RoundResult,
        king_entry: TrainedEntry,
        chal_entry: TrainedEntry,
        *,
        seed: int | str,
        round_id: str,
    ) -> RoundResult:
        """Fold the public-benchmark gate into a *winning* round result.

        Scores both sides on gift-eval (via the sidecar bridge) and runs the
        paired no-regression bootstrap. The gate is uncomputable β€” and the
        round therefore inconclusive under ``enforce`` β€” when either sidecar run
        fails, gift-eval was skipped/errored, or the two runs scored against
        different pinned data revisions (a consensus-safety check: king and
        challenger must be judged on identical public data).
        """
        from ..eval.gift_gate import evaluate_gift_gate, uncomputable_gate
        from ..eval.koth import apply_gift_gate

        p = self.cfg.koth_params()
        mode = p.gift_gate_mode
        king_run = self._gift_rows(king_entry)
        chal_run = self._gift_rows(chal_entry)
        if (
            king_run is None or chal_run is None
            or king_run.get("status") != "ok" or chal_run.get("status") != "ok"
        ):
            gate = uncomputable_gate(p.gift_gate_tolerance, "gift-eval sidecar unavailable/errored")
        elif king_run.get("revision") != chal_run.get("revision"):
            gate = uncomputable_gate(
                p.gift_gate_tolerance,
                f"data-revision mismatch: king {king_run.get('revision')} != "
                f"chal {chal_run.get('revision')}",
            )
        else:
            gate = evaluate_gift_gate(
                king_run["rows"], chal_run["rows"],
                tolerance=p.gift_gate_tolerance,
                alpha=p.bootstrap_alpha,
                B=p.bootstrap_B,
                seed=seed,
                min_configs=p.gift_gate_min_configs,
            )
        log.info(
            "gift-gate round=%s mode=%s computed=%s passed=%s lcb=%s tol=%.4f "
            "n_configs=%d king_agg=%.5f chal_agg=%.5f%s",
            round_id, mode, gate.computed, gate.passed,
            f"{gate.lcb:.5f}" if gate.computed else "n/a", gate.tolerance,
            gate.n_configs, gate.king_agg, gate.chal_agg,
            "" if gate.computed else f" reason={gate.reason!r}",
        )
        return apply_gift_gate(result, gate, mode=mode)

    def _maybe_run_benchmarks(
        self, manifest: TrainingManifest, outcome: RoundOutcome | None
    ) -> None:  # pragma: no cover β€” exercised only in the live loop
        """Log public-benchmark numbers for a newly crowned king (log-only).

        Best-effort and strictly off the consensus path: it runs only when a
        challenger just dethroned the king, scores that new king's checkpoint via
        the isolated sidecar, and logs whatever comes back. Any failure is
        swallowed β€” a benchmark hiccup must never disturb weights or KOTH state.
        """
        ec = self.cfg.eval
        if not ec.run_benchmarks:
            return
        if outcome is None or not outcome.transition.dethroned:
            return
        new_king = manifest.entry_for_role("challenger")
        if new_king is None:
            return
        try:
            from ..eval.benchmarks import format_report, run_benchmarks

            ckpt = self._fetch_checkpoint_dir(new_king)
            report = run_benchmarks(
                ckpt,
                project_dir=ec.benchmark_project_dir,
                suites=ec.benchmark_suites or ("gift-eval", "boom", "time"),
                num_samples=ec.benchmark_num_samples or ec.num_samples,
                max_series=ec.benchmark_max_series,
                device=self.device,
            )
            if report is not None:
                log.info(
                    "benchmarks round=%s king=%s %s",
                    manifest.round_id, self.state.king_hotkey, format_report(report),
                )
        except Exception as e:  # noqa: BLE001 β€” log-only, never fatal
            log.warning("benchmark hook failed for round=%s: %s", manifest.round_id, e)

    # ── Cascade: king-reign promotion ────────────────────────────────────────

    def _current_king_entry(self, manifest: TrainingManifest) -> TrainedEntry | None:
        """The manifest checkpoint the reigning champion produced this round.

        Cascade times the *validator's champion*, not the manifest's (lagging)
        king role β€” so the checkpoint to score is the entry whose miner hotkey is
        the champion. Prefers the primary throne size (what the benchmark sidecar
        scores) and falls back to any size that hotkey trained."""
        hk = self.state.king_hotkey
        if hk is None:
            return None
        matches = [e for e in manifest.entries if e.miner_hotkey == hk]
        if not matches:
            return None
        primary = self.cfg.throne_contracts()[0].arch_preset
        return next((e for e in matches if e.size == primary), matches[0])

    @staticmethod
    def _bench_scores_dict(entry: TrainedEntry) -> dict | None:
        """The six Cascade numbers off a manifest entry's trainer-signed
        ``bench_scores`` (GIFT-Eval / BOOM / TIME CRPS+MASE), or ``None`` when the
        entry carries none. This is the authoritative, consensus-safe source: every
        validator reads the identical signed numbers."""
        bs = entry.bench_scores
        if bs is None:
            return None
        return {
            "gifteval_crps": bs.gifteval_crps, "gifteval_mase": bs.gifteval_mase,
            "boom_crps": bs.boom_crps, "boom_mase": bs.boom_mase,
            "time_crps": bs.time_crps, "time_mase": bs.time_mase,
        }

    def _bench_metrics_via_sidecar(self, entry: TrainedEntry) -> dict | None:  # pragma: no cover β€” sidecar glue
        """Fallback: score one checkpoint on GIFT-Eval, BOOM, and TIME via the
        out-of-process sidecar, returning the six numbers or ``None`` when any suite
        is missing/errored. Used only when the manifest carries no ``bench_scores``
        (e.g. a trainer that predates the Cascade hook). NOTE: independently-run GPU
        sweeps are not bit-reproducible, so this path is not consensus-safe across
        validators β€” prefer the trainer-signed numbers."""
        from ..eval.benchmarks import extract_bench_scores, run_benchmarks

        ec = self.cfg.eval
        ckpt = self._fetch_checkpoint_dir(entry)
        num_samples = ec.benchmark_num_samples or ec.num_samples
        eval_host = self._eval_host()
        if eval_host is not None:
            # Offload the cascade bench (GIFT-Eval+BOOM+TIME) to the GPU pod,
            # same seam as the gift-eval gate; the wallet stays on this box.
            from .eval_offload import bench_scores_via_host

            metrics = bench_scores_via_host(
                eval_host, ckpt,
                num_samples=num_samples,
                max_series=ec.cascade_bench_max_series,  # 0 = full battery
                data_dir=(ec.gift_gate_data_dir or None),
                device="cuda",
                timeout_s=ec.gift_gate_timeout_s,
            )
        else:
            report = run_benchmarks(
                ckpt,
                project_dir=ec.benchmark_project_dir,
                suites=("gift-eval", "boom", "time"),
                num_samples=num_samples,
                max_series=ec.cascade_bench_max_series,  # 0 = full battery
                device=self.device,
            )
            metrics = extract_bench_scores(report)
        if metrics is None:
            log.warning("cascade: incomplete GIFT-Eval/BOOM/TIME metrics for king checkpoint %s; "
                        "not recording this round", entry.trained_pointer)
        return metrics

    def _record_king_checkpoint(
        self, manifest: TrainingManifest, now: float
    ) -> None:  # pragma: no cover β€” sidecar glue
        """Add the reigning king's checkpoint to the reign log so a later Cascade
        selection is a lookup, not a re-eval. Prefers the trainer's signed
        ``bench_scores`` on the manifest (consensus-safe); falls back to scoring via
        the local sidecar only when the manifest carries none. Best-effort: a miss
        just means this round's checkpoint isn't a promotion candidate."""
        if self.cascade is None or self.state.king_hotkey is None:
            return
        entry = self._current_king_entry(manifest)
        if entry is None:
            return
        metrics = self._bench_scores_dict(entry) or self._bench_metrics_via_sidecar(entry)
        if metrics is None:
            return
        self.cascade.record_checkpoint(entry.trained_pointer, now=now, size=entry.size, **metrics)

    def _cascade_round(
        self, manifest: TrainingManifest, outcome: RoundOutcome | None
    ) -> None:  # pragma: no cover β€” live-loop glue; the controller is unit-tested
        """One Cascade step, run at the end of a round (after weights/receipts).
        Resets the reign clock on a dethrone, records the reigning king's
        checkpoint, then checks the clock β€” a fired Cascade installs the promoted
        init and re-crowns the SAME king (DEC-CA-0004: the champion throne is
        never touched). Fully guarded: Cascade never disturbs KOTH."""
        if self.cascade is None:
            return
        import time

        now = time.time()
        try:
            # The reign clock runs on the round's epoch block β€” identical for every
            # validator (from the signed manifest), so all fire on the same round.
            block = self._epoch_start_block(manifest)
            # Reuse KOTH's dethrone signal to reset the clock (never reimplement it);
            # on genesis, crown the first champion so the reign clock starts ticking.
            if outcome is not None and outcome.transition.dethroned and outcome.transition.new_king_hotkey:
                self.cascade.note_dethrone(outcome.transition.new_king_hotkey, block=block)
            elif self.cascade.state.king_hotkey is None and self.state.king_hotkey is not None:
                self.cascade.note_dethrone(self.state.king_hotkey, block=block)
            self._record_king_checkpoint(manifest, now)
            event = self.cascade.cascade_check(block=block, now=now)
            if event is not None:
                self._apply_cascade(event)
        except Exception as e:  # noqa: BLE001 β€” Cascade must never disturb a round
            log.warning("cascade step failed for round=%s: %s", manifest.round_id, e)

    def _apply_cascade(self, event: object) -> None:  # pragma: no cover β€” live-loop glue
        """Log a fired Cascade. The champion throne is deliberately untouched
        (DEC-CA-0004): the king persists β€” vacating had no benefit (both roles
        train from the shared init) and a vacant throne refillable only via the
        dethrone branch froze the reign clock when the incumbent kept winning."""
        winner = getattr(event, "winner", None)
        king = getattr(event, "old_king", None)
        log.info(
            "cascade: promotion installed (king %s persists); field trains from "
            "checkpoint %s next round",
            (king or "?")[:12],
            getattr(winner, "checkpoint_id", "?"),
        )

    def process_round(
        self,
        manifest: TrainingManifest,
        windows: list[EvalWindow],
        base_seed: int | str,
    ) -> RoundOutcome | None:
        """Evaluate one manifest against the eval windows and update state.

        A round carries one (king, challenger) pair PER trained size (the primary
        plus any ``[[training.sizes]]``). Each size's pair is scored on the SAME
        windows, then the per-size scores are POOLED β€” king's across sizes vs
        challenger's across sizes, in identical order β€” and a single paired
        bootstrap decides ONE throne on the combined score (scaling-aware KOTH).
        Pooling preserves pairing because each size's king and challenger share
        the window ``abs_target``.

        Returns None (king holds, no state change) when the manifest carries no
        size with both a king and a challenger, or fails the contract gate.
        Otherwise returns the round outcome with the (already-applied) transition.
        """
        reason = self.check_manifest(manifest)
        if reason is not None:
            log.warning("rejecting manifest round=%s: %s", manifest.round_id, reason)
            return None

        king_by_size = {e.size: e for e in manifest.entries_for_role("king")}
        chal_by_size = {e.size: e for e in manifest.entries_for_role("challenger")}
        paired_sizes = [s for s in manifest.sizes() if s in king_by_size and s in chal_by_size]
        if not paired_sizes:
            log.info("manifest round=%s has no king/challenger pair; king holds", manifest.round_id)
            return None

        king_scores: list[WindowScore] = []
        chal_scores: list[WindowScore] = []
        score_records: list[EntryScores] = []
        for size in paired_sizes:
            import time as _time

            _t0 = _time.perf_counter()
            ks = self._evaluate(king_by_size[size], windows)
            _t_king = _time.perf_counter() - _t0
            _t1 = _time.perf_counter()
            cs = self._evaluate(chal_by_size[size], windows)
            _t_chal = _time.perf_counter() - _t1
            log.info(
                "round=%s eval-timing size=%s device=%s n_windows=%d num_samples=%d "
                "king=%.1fs challenger=%.1fs total=%.1fs",
                manifest.round_id, size, self.device, len(windows),
                self.cfg.eval.num_samples, _t_king, _t_chal, _t_king + _t_chal,
            )
            king_scores += ks
            chal_scores += cs
            for entry, scores in ((king_by_size[size], ks), (chal_by_size[size], cs)):
                score_records.append(EntryScores(
                    role=entry.role, size=size,
                    hotkey=entry.miner_hotkey, uid=entry.miner_uid,
                    scores=tuple(WindowScoreRecord.from_score(s) for s in scores),
                ))
        # One challenger generator competes at every size, so any size's entry
        # carries its identity for the KOTH state machine.
        chal_entry = chal_by_size[paired_sizes[0]]

        tenure_at_decision = self.state.tenure_rounds
        result = evaluate_round(
            king_scores,
            chal_scores,
            self.cfg.koth_params(),
            seed=base_seed,
            king_tenure_rounds=tenure_at_decision,
        )
        # Public-benchmark no-regression gate: only on a private-pool win, and
        # only when enabled. It can block a dethrone (or, uncomputable, hold the
        # round) but never grant one. Gated on the primary size's checkpoint
        # pair (the pooled decision spans sizes; the gate screens on one).
        if self.cfg.scoring.gift_gate_mode != "off" and result.challenger_wins_round:
            gate_size = (
                self.cfg.training.arch_preset
                if self.cfg.training.arch_preset in king_by_size and
                self.cfg.training.arch_preset in chal_by_size
                else paired_sizes[0]
            )
            result = self._run_gift_gate(
                result, king_by_size[gate_size], chal_by_size[gate_size],
                seed=base_seed, round_id=manifest.round_id,
            )
        transition = state_mod.apply_round(
            self.state,
            challenger_hotkey=chal_entry.miner_hotkey,
            challenger_uid=chal_entry.miner_uid,
            result=result,
            dethrone_cp=self.cfg.scoring.dethrone_cp,
            keep_former_kings=self.cfg.scoring.reward_prior_kings,
        )
        self.state = transition.state
        log.info(
            "round=%s lcb=%.4f margin=%.4f win=%s %s king=%s tenure=%d",
            manifest.round_id, result.lcb, result.margin, result.challenger_wins_round,
            transition.note, self.state.king_hotkey, self.state.tenure_rounds,
        )
        # Shadow diagnostics: never gate the verdict. A rank-based view that
        # disagrees with the LCB, or a per-domain win-rate sign flip, means the
        # pool composition is doing the deciding β€” alert-worthy, not decisive.
        if result.win_rate is not None:
            log.info(
                "round=%s diag n_clusters=%d win_rate=%.3f wilcoxon_p=%s per_domain=%s",
                manifest.round_id, result.n_clusters, result.win_rate,
                f"{result.wilcoxon_p:.4g}" if result.wilcoxon_p is not None else "n/a",
                {d: f"{wr:.2f}/n{n}" for d, (wr, n) in (result.per_domain_win_rate or {}).items()},
            )
        return RoundOutcome(
            result=result, transition=transition, entry_scores=tuple(score_records),
            king_tenure_rounds=tenure_at_decision,
        )

    def _epoch_start_block(self, manifest: TrainingManifest) -> int:
        """The round's epoch-boundary block: ``created_block`` floored to the
        epoch grid. Monotonic and identical for every validator (from the shared
        manifest), so it is the consensus key for daily eval-pool snapshot
        selection β€” unlike the round id, which is a block *hash* (non-monotonic).
        """
        epoch_blocks = max(1, self.cfg.round.epoch_blocks)
        return (manifest.created_block // epoch_blocks) * epoch_blocks

    # ── public round receipts ────────────────────────────────────────────────

    def build_round_receipt(
        self,
        manifest: TrainingManifest,
        *,
        base_seed: int,
        epoch_start_block: int,
        epoch_block_hash: str,
        outcome: RoundOutcome | None = None,
        windows: list[EvalWindow] | None = None,
        reject_reason: str | None = None,
        participants: tuple[Participant, ...] = (),
        pool_provenance: tuple[str, str] = ("", ""),
        reward_uids: tuple[int, ...] = (),
        weights: tuple[float, ...] = (),
        validator_hotkey: str = "",
    ) -> RoundReceipt:
        """Assemble the round's public receipt (pure β€” no I/O, no signing).

        A gated-out manifest β€” or one with no (king, challenger) pair to score β€”
        yields a ``rejected`` receipt carrying the reason; a scored round yields
        the full record: chain context, embedded manifest, participant set, the
        eval slice, every per-window score, the verdict, and the weight vector.
        """
        from ..trainer.contract import RoundSeeds

        seeds = RoundSeeds.derive(base_seed, self.cfg.training)
        if reject_reason is not None:
            return build_receipt(
                round_id=manifest.round_id, status="rejected",
                epoch_start_block=epoch_start_block, epoch_block_hash=epoch_block_hash,
                base_seed=base_seed, seeds=seeds, manifest=manifest,
                participants=participants, reject_reason=reject_reason,
                reward_uids=reward_uids, weights=weights,
                validator_hotkey=validator_hotkey,
            )
        if outcome is None or windows is None:
            raise ValueError("a scored receipt needs both outcome and windows")
        eval_context = EvalContext(
            pool_ref=pool_provenance[0],
            pool_digest=pool_provenance[1],
            window_ids=tuple(w.series_id for w in windows),
            n_windows=len(windows),
            num_samples=self.cfg.eval.num_samples,
        )
        verdict = VerdictRecord.from_round(
            outcome.result, outcome.transition,
            params=self.cfg.koth_params(), bootstrap_seed=base_seed,
            king_tenure_rounds=outcome.king_tenure_rounds,
        )
        return build_receipt(
            round_id=manifest.round_id, status="scored",
            epoch_start_block=epoch_start_block, epoch_block_hash=epoch_block_hash,
            base_seed=base_seed, seeds=seeds, manifest=manifest,
            participants=participants, eval_context=eval_context,
            entry_scores=outcome.entry_scores, verdict=verdict,
            reward_uids=reward_uids, weights=weights,
            validator_hotkey=validator_hotkey,
        )

    def _publish_round_receipt(
        self,
        client: object,
        manifest: TrainingManifest,
        base_seed: int,
        *,
        outcome: RoundOutcome | None = None,
        windows: list[EvalWindow] | None = None,
        reject_reason: str | None = None,
        window_source: object = None,
        reward_uids: tuple[int, ...] = (),
        weights: tuple[float, ...] = (),
    ) -> None:  # pragma: no cover β€” live-loop glue; assembly is unit-tested
        """Gather chain context, sign, and publish the round receipt.

        Best-effort by design: a receipt failure must never disturb weights or
        KOTH state (they are already committed), so every chain lookup degrades
        to an empty field and any publish error is logged and swallowed. The
        audit CLI treats missing context as WARN, not PASS.
        """
        from datetime import datetime

        from ..shared.hippius import (
            open_manifest_store,
            publish_receipt,
            update_receipt_index,
        )
        from ..shared.receipt import dump_receipt, sign_receipt, summarize_receipt

        try:
            epoch_start = self._epoch_start_block(manifest)
            epoch_hash = ""
            current_block: int | None = None
            participants: tuple[Participant, ...] = ()
            try:
                epoch_hash = client.block_hash(epoch_start)
                participants = participants_from_commitments(
                    client.poll_commitments(include_history=True),
                    cutoff_block=epoch_start,
                    floor_block=self.cfg.round.commit_floor_block,
                )
                # Anchor for the dashboard's next-round countdown (best-effort;
                # the client extrapolates block→wall-clock from this + as_of).
                current_block = int(client.current_block())
            except Exception as e:  # noqa: BLE001 β€” chain context is best-effort
                log.warning("receipt chain context unavailable for round=%s: %s",
                            manifest.round_id, e)
            provenance = ("", "")
            prov_fn = getattr(window_source, "provenance_for_round", None)
            if prov_fn is not None:
                try:
                    # Same epoch block that selected the round's windows, so the
                    # recorded provenance is the pool actually scored.
                    provenance = tuple(prov_fn(base_seed, block=epoch_start))
                except Exception as e:  # noqa: BLE001
                    log.warning("pool provenance unavailable for round=%s: %s",
                                manifest.round_id, e)
            wallet = getattr(client, "wallet", lambda: None)()
            hotkey_ss58 = str(getattr(getattr(wallet, "hotkey", None), "ss58_address", "") or "")

            receipt = self.build_round_receipt(
                manifest,
                base_seed=base_seed,
                epoch_start_block=epoch_start,
                epoch_block_hash=str(epoch_hash),
                outcome=outcome,
                windows=windows,
                reject_reason=reject_reason,
                participants=participants,
                pool_provenance=(provenance[0], provenance[1]),
                reward_uids=reward_uids,
                weights=weights,
                validator_hotkey=hotkey_ss58,
            )
            if wallet is not None:
                receipt = sign_receipt(receipt, wallet)
            else:
                log.warning("publishing an UNSIGNED receipt (no wallet) for round=%s",
                            manifest.round_id)
            store = open_manifest_store(self.cfg.storage)
            key = publish_receipt(store, dump_receipt(receipt), manifest.round_id,
                                  validator_hotkey=hotkey_ss58)
            log.info("published %s receipt round=%s signed=%s β†’ s3://%s/%s",
                     receipt.status, manifest.round_id, receipt.signature is not None,
                     self.cfg.storage.manifest_bucket, key)
            # Refresh the dashboard-facing rolling index (best-effort, and inside
            # the outer guard: a listing convenience must never disturb a round).
            try:
                now_iso = datetime.now(UTC).isoformat(timespec="seconds")
                # Schedule anchor for the "time until next round" countdown. The
                # next round begins at the next epoch boundary; the client turns
                # blocks into wall-clock via block_time_s, extrapolating current_block
                # from `as_of`. Bittensor blocks are ~12s regardless of epoch_blocks
                # (testnet shortens epochs, not block time), so it is a constant.
                chain = {
                    "as_of": now_iso,
                    "current_block": current_block,
                    "epoch_start_block": epoch_start,
                    "epoch_blocks": int(self.cfg.round.epoch_blocks),
                    "block_time_s": 12.0,
                }
                update_receipt_index(
                    store, summarize_receipt(receipt),
                    updated_at=now_iso,
                    subnet={"netuid": self.cfg.subnet.netuid, "name": self.cfg.subnet.name},
                    chain=chain,
                )
            except Exception as e:  # noqa: BLE001
                log.warning("receipt index update failed for round=%s: %s",
                            manifest.round_id, e)
        except Exception as e:  # noqa: BLE001 β€” receipts must never disturb the round
            log.warning("receipt publication failed for round=%s: %s", manifest.round_id, e)

    # ── live loop ────────────────────────────────────────────────────────────

    def _publish_chain_status(self, client: object, store: object) -> None:  # pragma: no cover
        """Publish the dashboard's live ``status/chain.json`` (current block,
        epoch grid, stage windows, revealed submissions) on the poll cadence.

        Purely presentational and best-effort β€” it feeds the web dashboard's
        round-stage strip and live submissions panel between receipts, and any
        failure (chain flake, storage outage) is swallowed: status telemetry
        must never disturb a round.
        """
        from datetime import datetime

        from ..shared.chain_status import build_chain_status, publish_chain_status

        try:
            status = build_chain_status(
                self.cfg,
                current_block=int(client.current_block()),  # type: ignore[attr-defined]
                commitments=client.poll_commitments(),  # type: ignore[attr-defined]
                network=str(getattr(client, "network", "")),
                as_of=datetime.now(UTC).isoformat(timespec="seconds"),
            )
            publish_chain_status(store, status)
        except Exception as e:  # noqa: BLE001 β€” telemetry only
            log.debug("chain status publish skipped: %s", e)

    def run_forever(self, client: object, *, window_source: object) -> None:  # pragma: no cover
        """Poll the manifest bucket β†’ evaluate β†’ set weights, once per round.

        ``window_source`` is a :class:`cascade.validator.windows.WindowSource`
        (the loaded private pool). Each new manifest's ``round_id`` is the base
        seed; the same seed drives the rotating window slice so every validator
        scores the identical set.
        """
        import time

        from ..shared.hippius import StorageError, open_manifest_store, read_latest_manifest
        from ..shared.manifest import load_manifest

        store = open_manifest_store(self.cfg.storage)
        poll = self.cfg.manifest.poll_seconds
        # Dedup on CONTENT, not round_id: a re-published manifest for an
        # already-seen round id (same-round-id rerun, e.g. after a contract
        # fix) must be re-judged, not silently skipped (2026-07-15: the
        # round_id-only latch ignored the rerun manifest with no log line).
        last_round: str | None = None
        last_digest: str | None = None
        while True:
            try:
                # Live dashboard telemetry first, every poll: between receipts
                # this is the page's only fresh view of the chain (stage strip
                # + live submissions). Best-effort; never affects the round.
                self._publish_chain_status(client, store)
                raw = read_latest_manifest(store)
                digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
                if digest == last_digest:
                    log.debug("manifest unchanged (round=%s sha=%s…); skipping",
                              last_round, digest[:12])
                else:
                    manifest = load_manifest(raw)
                    base_seed = int(manifest.round_id)
                    if manifest.round_id == last_round:
                        log.warning(
                            "manifest for already-handled round=%s RE-PUBLISHED "
                            "with different content (sha %s… -> %s…); re-judging",
                            manifest.round_id,
                            (last_digest or "")[:12], digest[:12],
                        )
                    log.info(
                        "new manifest round=%s entries=%d (%s); gating + scoring …",
                        manifest.round_id, len(manifest.entries),
                        ",".join(f"{e.role}:uid{e.miner_uid}" for e in manifest.entries),
                    )
                    # Gate first so a rejected manifest never moves weights.
                    reason = self.check_manifest(manifest)
                    retry_pin = False
                    if reason is None:
                        # Pool-pin gate: the signed snapshot pin must match this
                        # validator's own deterministic selection for the round.
                        try:
                            reason = self.check_pool_pin(
                                manifest, window_source,
                                block=self._epoch_start_block(manifest),
                            )
                        except StorageError as e:
                            # The index could not be READ (auth/network/5xx) β€”
                            # a transient, not a verdict. Within the grace
                            # window: no latch, no receipt, retry next poll.
                            reason = self._pool_pin_read_failed(manifest.round_id, e)
                            retry_pin = reason is None
                        else:
                            self._pin_read_first_failure.pop(str(manifest.round_id), None)
                    if retry_pin:
                        # In-grace read failure: neither last_round nor
                        # last_digest move, so the next manifest poll re-judges
                        # this round from scratch. Falls through to the weight
                        # re-assert + sleep below.
                        pass
                    elif reason is not None:
                        log.warning("rejecting manifest round=%s: %s", manifest.round_id, reason)
                        last_round, last_digest = manifest.round_id, digest
                        # A rejected round still gets a public receipt carrying
                        # the gate's reason β€” visible, not silently absent.
                        self._publish_round_receipt(
                            client, manifest, base_seed,
                            reject_reason=reason, window_source=window_source,
                        )
                    elif not self.king_synced(manifest):
                        # The trainer trained the OLD king (incentive lags a
                        # dethrone). Hold the KOTH state and keep voting the champion
                        # so incentive migrates and the trainer re-syncs β€” bounded by
                        # the safety valve (see _resync_step). A public receipt
                        # records why, not a silent skip.
                        last_round, last_digest = manifest.round_id, digest
                        self.state, reject_reason = self._resync_step(manifest)
                        self._persist_state()
                        reward_uids = self._reward_uids(manifest, None, client)
                        weights_vec = self._apply_weights(client, manifest.round_id, reward_uids)
                        self._publish_round_receipt(
                            client, manifest, base_seed,
                            reject_reason=reject_reason,
                            window_source=window_source,
                            reward_uids=tuple(reward_uids), weights=weights_vec,
                        )
                    else:
                        # The epoch block selects the daily snapshot; base_seed
                        # rotates the window slice within it.
                        windows = window_source.windows_for_round(
                            base_seed, self.cfg.eval.n_windows,
                            block=self._epoch_start_block(manifest),
                        )
                        # process_round mutates the sticky KOTH state atomically (it
                        # raises before any mutation on a transient eval/fetch error,
                        # leaving state untouched for a clean retry). Mark the round
                        # consumed as soon as it returns, so a later weight-set failure
                        # can NEVER re-run it and double-count the streak/tenure.
                        outcome = self.process_round(manifest, windows, base_seed)
                        # Back in sync β€” clear any accumulated resync holds so a
                        # future desync starts the safety-valve count from zero.
                        if self.state.resync_holds or self.state.last_resync_round_id:
                            self.state = replace(
                                self.state, resync_holds=0, last_resync_round_id=None)
                        last_round, last_digest = manifest.round_id, digest
                        self._persist_state()
                        reward_uids = self._reward_uids(manifest, outcome, client)
                        weights_vec = self._apply_weights(client, manifest.round_id, reward_uids)
                        # The public receipt β€” strictly after weights, so it
                        # records what was actually set (empty vector = the
                        # weight extrinsic failed this round).
                        if outcome is not None:
                            self._publish_round_receipt(
                                client, manifest, base_seed,
                                outcome=outcome, windows=windows,
                                window_source=window_source,
                                reward_uids=tuple(reward_uids), weights=weights_vec,
                            )
                        else:
                            # Gated in but nothing to score (no king/challenger
                            # pair at any size): a public record still exists.
                            self._publish_round_receipt(
                                client, manifest, base_seed,
                                reject_reason="no_king_challenger_pair",
                                window_source=window_source,
                                reward_uids=tuple(reward_uids), weights=weights_vec,
                            )
                        # Log-only public benchmarks for a freshly crowned king.
                        # Strictly after weights are decided; never affects them.
                        self._maybe_run_benchmarks(manifest, outcome)
                        # Cascade step β€” strictly last, so this round's weights and
                        # receipt already recorded the outgoing king. Resets the
                        # reign clock on a dethrone, records the king's checkpoint,
                        # and fires the promotion when the clock is ripe.
                        self._cascade_round(manifest, outcome)
            except Exception as e:  # noqa: BLE001 β€” a service loop must not die on one round
                log.exception("round processing failed; retrying after poll: %s", e)
            try:
                self._maybe_reassert_weights(client)
            except Exception as e:  # noqa: BLE001
                log.warning("weight re-assert check failed: %s", e)
            time.sleep(poll)

    def _maybe_reassert_weights(self, client: object) -> None:
        """Re-push the standing weight vector every ``weight_set_interval_blocks``.

        Subtensor treats a validator whose ``last_update`` is older than the
        subnet's ``activity_cutoff`` (5000 blocks β‰ˆ 16.7 h on netuid 91) as
        inactive in Yuma consensus. A mainnet round is 7200 blocks, so voting
        only when a manifest lands blows through the cutoff every round β€” and a
        stalled trainer silences the validator entirely. The vector needs no
        manifest: it is recomputed from the persisted champion state (king +
        registered prior kings; no champion burns to ``burn_uid``), so this is
        a pure freshness signal that can never move the throne. The interval
        must stay β‰₯ the subnet's ``weights_rate_limit`` (100 blocks on netuid
        91) or the chain silently no-ops the extrinsic; ≀ 0 disables.
        """
        interval = int(self.cfg.validator.weight_set_interval_blocks)
        if interval <= 0:
            return
        cur = int(client.current_block())  # type: ignore[attr-defined]
        if self._last_weight_block is not None and cur - self._last_weight_block < interval:
            return
        uids: list[int] = []
        if self.state.king_hotkey is not None:
            uid = client.uid_for_hotkey(self.state.king_hotkey)  # type: ignore[attr-defined]
            if uid is not None:
                uids.append(uid)
        for hk in self.state.former_kings:
            uid = client.uid_for_hotkey(hk)  # type: ignore[attr-defined]
            if uid is not None and uid not in uids:
                uids.append(uid)
        log.info("re-asserting weights (last set %s, block %d): reward_uids=%s",
                 "never" if self._last_weight_block is None else
                 f"{cur - self._last_weight_block} blocks ago", cur,
                 uids or [self.cfg.scoring.burn_uid])
        self._apply_weights(client, "weight-reassert", uids)
        # Stamp even when the extrinsic failed: the next attempt comes after
        # one interval (~27 more before the cutoff), not every poll tick.
        self._last_weight_block = cur

    @staticmethod
    def _manifest_king_hotkey(manifest: TrainingManifest) -> str | None:
        e = manifest.entry_for_role("king")
        return e.miner_hotkey if e is not None else None

    @staticmethod
    def _manifest_king_uid(manifest: TrainingManifest) -> int | None:
        e = manifest.entry_for_role("king")
        return e.miner_uid if e is not None else None

    def king_synced(self, manifest: TrainingManifest) -> bool:
        """Whether the round's *trained* king matches the validator's champion.

        The trainer picks the king it trains from on-chain incentive, which lags
        the validator's dethrone verdicts. Until the champion
        the validator crowned actually becomes the highest-incentive UID β€” and so
        the king the trainer trains β€” the two disagree, and a round trained
        against the *old* king must not have its verdict applied to the *new*
        champion. Synced when the champion is unset (bootstrap) or the trained
        king is the champion.
        """
        if self.state.king_hotkey is None:
            return True
        return self._manifest_king_hotkey(manifest) == self.state.king_hotkey

    def _resync_step(self, manifest: TrainingManifest) -> tuple[ChampionState, str]:
        """Next champion state + receipt reason for a king-resync round.

        Called when the trained king != champion (``king_synced`` is False). By
        default it holds the throne, bumping the consecutive-hold counter, and the
        caller keeps voting the champion so incentive migrates and the trainer
        re-syncs. SAFETY VALVE: once the champion has stayed un-synced for
        ``scoring.king_resync_max_rounds`` consecutive rounds it can never be the
        king the trainer trains (e.g. it has no usable commitment), so holding
        forever would wedge the subnet β€” the valve abandons it and adopts the
        trainer's trained king (:func:`state.demote_to_trained`), and normal
        scoring resumes next round. ``king_resync_max_rounds <= 0`` disables the
        valve (hold indefinitely). The counter advances once per DISTINCT
        un-synced round: a restart re-gates the same stale manifest, and
        counting those re-gates let five restarts during a pause trip the
        valve and demote a healthy champion (2026-07-22). Pure: returns the
        new state; the caller persists, votes, and publishes.
        """
        champ = self.state.king_hotkey
        trained = self._manifest_king_hotkey(manifest)
        round_id = str(manifest.round_id)
        same_round = self.state.last_resync_round_id == round_id
        holds = self.state.resync_holds if same_round else self.state.resync_holds + 1
        cap = self.cfg.scoring.king_resync_max_rounds
        if 0 < cap <= holds and trained is not None:
            log.warning(
                "round=%s king_resync SAFETY VALVE: champion %s un-synced %d rounds "
                "(cap=%d) β€” demoting to trained king %s, resuming normal scoring next round",
                manifest.round_id, (champ or "?")[:12], holds, cap, (trained or "?")[:12],
            )
            return (
                state_mod.demote_to_trained(
                    self.state, trained_hotkey=trained,
                    trained_uid=self._manifest_king_uid(manifest),
                ),
                f"king_resync_demoted: champion {champ} un-synced {holds} rounds "
                f"(cap={cap}); adopted trained king {trained}",
            )
        log.warning(
            "round=%s trainer king %s != champion %s; voting champion to re-sync "
            "incentive, KOTH state held (%d/%s)",
            manifest.round_id, (trained or "?")[:12], (champ or "?")[:12],
            holds, cap if cap > 0 else "∞",
        )
        return (
            replace(self.state, resync_holds=holds, last_resync_round_id=round_id),
            f"king_resyncing: champion {champ} != trained king {trained}",
        )

    def _king_uid_to_vote(self, manifest: TrainingManifest, *, client: object | None = None) -> int | None:
        """The UID to put the king's weight on this round.

        The **validator's champion state** is the authority on who holds the
        throne, so vote *that* king every round β€” not the (lagging) king the
        trainer happened to train. This is what makes a dethrone STICK: the new
        champion keeps the weight, incentive migrates to it, and next round the
        trainer trains it as king (they re-sync). Voting the trained/manifest
        king instead β€” the old behaviour β€” reverted a dethrone the moment the
        trainer lagged one round, orphaning the champion. The champion hotkey is
        resolved to its current UID via the metagraph (robust to re-registration);
        the manifest king is used only to bootstrap when there is no champion yet.
        """
        if self.state.king_hotkey is not None:
            if client is not None:
                resolved = client.uid_for_hotkey(self.state.king_hotkey)  # type: ignore[attr-defined]
                if resolved is not None:
                    return resolved
            return self.state.king_uid
        king_entry = manifest.entry_for_role("king")
        return king_entry.miner_uid if king_entry is not None else None

    def _reward_uids(
        self, manifest: TrainingManifest, outcome: RoundOutcome | None, client: object
    ) -> list[int]:
        """UIDs that share this round's weight: the current king plus any
        ``former_kings`` still registered (teutonic-style equal-share payout).

        Returns an empty list when there is no king to vote for at all (no
        champion and no manifest king); the loop hands that to
        ``set_equal_share_weights``, which burns to ``burn_uid`` rather than
        reverting. The list is otherwise deduped/range-checked there too.

        ``[validator] force_burn`` empties the list HERE β€” not just at the
        weight push β€” so the published receipt's ``reward_uids`` agree with the
        burn vector actually set (``cascade-audit`` recomputes one from the
        other and fails on a mismatch).
        """
        if self.cfg.validator.force_burn:
            return []
        uids: list[int] = []
        king_uid = self._king_uid_to_vote(manifest, client=client)
        if king_uid is not None:
            uids.append(king_uid)
        for hk in self.state.former_kings:
            uid = client.uid_for_hotkey(hk)  # type: ignore[attr-defined]
            if uid is not None:
                uids.append(uid)
        return uids

    def _apply_weights(self, client: object, round_id: str, reward_uids: list[int]) -> tuple[float, ...]:
        """Set the equal-share weight vector on chain; return it (empty on failure).

        Shared by the scored path and the king-resync path. Always sets weights β€”
        an empty ``reward_uids`` burns to ``burn_uid`` so emission still leaves the
        network. A failed extrinsic is logged and retried next round (the empty
        vector is recorded truthfully in the receipt).

        ``[validator] force_burn`` overrides the vector to a burn HERE β€” the
        single choke point every push flows through (scored, resync, re-assert) β€”
        so the receipt records the burn that was actually set. Champion state is
        never touched by this override."""
        from ..shared.chain import decayed_share_vector

        if self.cfg.validator.force_burn:
            log.warning(
                "FORCE-BURN active (round=%s): %sburning to uid %d (champion state "
                "untouched β€” unset [validator] force_burn and restart to resume voting)",
                round_id,
                f"dropping reward_uids={reward_uids}; " if reward_uids else "",
                self.cfg.scoring.burn_uid,
            )
            reward_uids = []

        decay = self.cfg.scoring.king_decay
        try:
            n_uids = client.n_uids()  # type: ignore[attr-defined]
            client.set_equal_share_weights(  # type: ignore[attr-defined]
                reward_uids, n_uids, decay=decay, burn_uid=self.cfg.scoring.burn_uid,
            )
            vec = tuple(decayed_share_vector(
                reward_uids, n_uids, decay=decay, burn_uid=self.cfg.scoring.burn_uid))
            log.info("round=%s weights set: reward_uids=%s (n_uids=%d, burn_uid=%d)",
                     round_id, reward_uids or [self.cfg.scoring.burn_uid], n_uids,
                     self.cfg.scoring.burn_uid)
            # Reset the re-assert timer (fake test clients lack current_block).
            with contextlib.suppress(Exception):
                self._last_weight_block = int(client.current_block())  # type: ignore[attr-defined]
            return vec
        except Exception as e:  # noqa: BLE001 β€” retried next round
            log.warning("weight set failed for round=%s (king holds, retried next round): %s",
                        round_id, e)
            return ()

    def _persist_state(self) -> None:  # pragma: no cover
        from . import state as state_mod

        try:
            Path(self.cfg.validator.state_db_path).write_text(
                state_mod.dumps(self.state), encoding="utf-8"
            )
        except Exception as e:  # noqa: BLE001
            log.warning("failed to persist validator state: %s", e)


def _load_state(path: str) -> ChampionState:
    """Load persisted champion state from ``state_db_path`` (JSON), or a fresh
    state if the file is absent/unreadable."""
    p = Path(path)
    if not p.is_file():
        return ChampionState()
    try:
        return state_mod.loads(p.read_text(encoding="utf-8"))
    except Exception as e:  # noqa: BLE001
        log.warning("could not load validator state from %s (%s); starting fresh", path, e)
        return ChampionState()


def _bootstrap_state_from_receipts(store: object, anchor: str) -> ChampionState | None:
    """Champion inherited from the signed public receipt trail, or ``None``.

    First-boot inheritance for a validator with no local state: the throne
    otherwise lives only in each validator's private state DB, so a validator
    joining mid-reign would judge the next manifest blind (``king_synced``
    treats an unset champion as synced) and crown whichever king it happened
    to see win first β€” a different champion than every validator that
    witnessed the real dethrone (OPSLOG 2026-07-17).

    ``anchor`` is the pinned receipt-signing ss58 (``[manifest]
    validator_hotkey``, falling back to ``trainer_hotkey``) β€” the same trust
    anchor the validator already applies to manifests, extended once, at
    first boot, to the receipt trail. Reads the anchor's
    ``receipts/<anchor>/latest.json`` (legacy shared pointer as fallback) and
    adopts the throne recorded by a *scored* receipt whose signature
    verifies. When ``latest.json`` is a hold/rejected receipt (verdict-less
    by construction), the receipt *index* is consulted β€” but only as an
    UNTRUSTED pointer to candidate round ids: nothing is adopted except from
    a per-round receipt whose signature verifies against the anchor.

    Anything short of that β€” missing objects, unreadable JSON, a bad
    signature, a genesis throne (``king_hotkey`` unset) β€” returns ``None``
    and the caller proceeds with the stock blank-slate behaviour. Storage
    faults must never block validator startup.
    """
    if not anchor:
        return None
    from ..shared.hippius import (
        RECEIPT_INDEX_KEY,
        RECEIPT_LATEST_KEY,
        receipt_latest_key,
        receipt_round_key,
    )
    from ..shared.receipt import load_receipt, verify_receipt_signature

    def _adopt_from(key: str) -> ChampionState | None:
        try:
            text = store.get_text(key)
        except Exception:  # noqa: BLE001 β€” absent/unreachable key β‡’ next candidate
            return None
        try:
            receipt = load_receipt(text)
        except Exception as e:  # noqa: BLE001
            log.warning("receipt bootstrap: unreadable receipt at %s (%s); skipped", key, e)
            return None
        if not verify_receipt_signature(receipt, anchor):
            log.warning("receipt bootstrap: receipt at %s is not signed by the pinned "
                        "hotkey %s…; skipped", key, anchor[:8])
            return None
        v = receipt.verdict
        if receipt.status != "scored" or v is None or not v.king_hotkey or v.king_uid is None:
            return None
        log.info("receipt bootstrap: adopting champion %s (uid %d) from signed scored "
                 "receipt round=%s", v.king_hotkey, int(v.king_uid), receipt.round_id)
        return ChampionState(king_hotkey=str(v.king_hotkey), king_uid=int(v.king_uid))

    adopted = _adopt_from(receipt_latest_key(anchor)) or _adopt_from(RECEIPT_LATEST_KEY)
    if adopted is not None:
        return adopted
    try:
        rows = json.loads(store.get_text(RECEIPT_INDEX_KEY)).get("rounds", [])
    except Exception:  # noqa: BLE001 β€” no index β‡’ nothing more to try
        rows = []
    # Index rows are chronological (oldest first, capped at most-recent);
    # walk newest-first and adopt the first scored round that verifies.
    for row in reversed(rows):
        if str(row.get("status")) != "scored":
            continue
        adopted = _adopt_from(receipt_round_key(str(row.get("round_id", "")), anchor))
        if adopted is not None:
            return adopted
    return None


def _warm_start_installer(path: Path) -> Callable[[object], None]:
    """The default Cascade installer: promote the winning checkpoint by writing its
    pointer (and its eval numbers) to ``warm_start_init_path`` β€” the seam the
    trainer reads to warm-start every subsequent round from. Promotes AS-IS; no
    retrain/fine-tune."""

    def _install(winner: object) -> None:  # pragma: no cover β€” file glue
        import time

        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(
            json.dumps(
                {
                    "checkpoint_id": getattr(winner, "checkpoint_id", None),
                    "size": getattr(winner, "size", ""),
                    "score": getattr(winner, "score", None),
                    "gifteval_crps": getattr(winner, "gifteval_crps", None),
                    "gifteval_mase": getattr(winner, "gifteval_mase", None),
                    "boom_crps": getattr(winner, "boom_crps", None),
                    "boom_mase": getattr(winner, "boom_mase", None),
                    "time_crps": getattr(winner, "time_crps", None),
                    "time_mase": getattr(winner, "time_mase", None),
                    "installed_at": time.time(),
                },
                sort_keys=True,
            ),
            encoding="utf-8",
        )
        log.info("cascade: warm-start init written to %s (checkpoint %s)",
                 path, getattr(winner, "checkpoint_id", "?"))

    return _install


def _build_cascade(cfg: ChainConfig) -> CascadeController:
    """Construct the Cascade controller from config, restoring the persisted reign
    clock + checkpoint log so it resumes across restarts."""
    from .cascade import CascadeController, load_state

    state_path = Path(cfg.validator.cascade_state_db_path)
    return CascadeController(
        reign_days=cfg.scoring.cascade_reign_days,
        state=load_state(state_path),
        install_fn=_warm_start_installer(Path(cfg.validator.warm_start_init_path)),
        state_path=state_path,
    )


def build_runner(
    *,
    chain_toml: Path | None = None,
    cache_dir: Path | None = None,
    device: str = "cpu",
    eval_host_fn: Callable[[], RemoteHost | None] | None = None,
) -> ValidatorRunner:
    """Construct a runner from ``chain.toml``, restoring persisted champion
    state. Wallet/chain wiring for live weight-setting is attached by
    ``cascade-validator`` (see main.py). ``eval_host_fn`` (optional) resolves
    the GPU pod to offload heavy evals to β€” re-invoked per eval, so an elastic
    provisioner-rented pod is picked up lazily; the wallet stays on this box."""
    from ..shared.config import load_chain_config

    cfg = load_chain_config(chain_toml)
    state = _load_state(cfg.validator.state_db_path)
    if (cfg.validator.bootstrap_from_receipts and state.king_hotkey is None
            and state.rounds_seen == 0 and not state.former_kings):
        # Truly fresh validator (no champion, no history): inherit the throne
        # from the signed receipt trail before the first manifest is judged.
        from ..shared.hippius import open_manifest_store

        anchor = cfg.manifest.validator_hotkey or cfg.manifest.trainer_hotkey
        try:
            adopted = _bootstrap_state_from_receipts(
                open_manifest_store(cfg.storage), anchor)
        except Exception as e:  # noqa: BLE001 β€” storage must never block startup
            log.warning("receipt bootstrap skipped (%s); starting blank", e)
            adopted = None
        if adopted is not None:
            state = adopted
    # Cascade is opt-in ([scoring] cascade_enabled); off β‡’ no controller is wired
    # and the runner is pure KOTH.
    cascade = _build_cascade(cfg) if cfg.scoring.cascade_enabled else None
    return ValidatorRunner(
        cfg=cfg, state=state,
        cache_dir=cache_dir, device=device, cascade=cascade, eval_host_fn=eval_host_fn,
    )