dig-slashing 0.1.0

Validator slashing, attestation participation, inactivity accounting, and fraud-proof appeals for the DIG Network L2 blockchain.
Documentation
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
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
# dig-slashing Specification

**Version:** 0.4.0
**Status:** Draft
**Date:** 2026-04-16

## 1. Overview

`dig-slashing` is a self-contained Rust crate for the DIG Network L2 blockchain. It owns **validator slashing**, **optimistic slash lifecycle with fraud-proof appeal**, **Ethereum-parity attestation participation accounting with rewards and missed-attestation penalties**, **Ethereum-parity inactivity accounting**, **validator-local slashing protection**, and **slashing-evidence + slash-appeal mempool/block admission**.

**Scope:** consensus-level validator concerns only. DFSP / storage-provider slashing is out of scope.

The crate **does** own:

- **Offense catalogue** (`OffenseType`): `ProposerEquivocation`, `InvalidBlock`, `AttesterDoubleVote`, `AttesterSurroundVote`. Four discrete, cryptographically-provable offenses.
- **Evidence envelopes + payloads**: `SlashingEvidence` wrapper over `ProposerSlashing`, `AttesterSlashing`, `InvalidBlockProof`. Ethereum-parity shapes (`Checkpoint`, `AttestationData`, `IndexedAttestation`, `SignedBlockHeader`).
- **Per-offense deterministic verifiers**.
- **Optimistic slashing lifecycle** — base slash applied immediately on evidence inclusion; challenge window opens; `SlashingManager` tracks slash state through `Submitted → Accepted → ChallengeOpen → (Reverted | Finalised)`.
- **Fraud-proof appeal system**`SlashAppeal` with payload variants mirroring each offense; `verify_appeal` adjudicates the fraud proof; `AppealAdjudicator` applies reversal or upholds the slash, routing reporter and appellant bonds.
- **Pending-slash book**`PendingSlashBook` keyed by evidence hash, tracks lifecycle status, timers, bond escrow tags, and appeal history.
- **Attestation participation accounting**`ParticipationFlags` bitmask (`TIMELY_SOURCE`, `TIMELY_TARGET`, `TIMELY_HEAD`), `ParticipationTracker` (previous-epoch + current-epoch arrays), Ethereum-parity base reward formula, per-flag rewards/penalties, proposer inclusion reward.
- **Inactivity accounting**`InactivityScoreTracker` per-validator score, `in_finality_stall` detection, per-epoch inactivity penalty formula. Continuous accounting; **not** event-driven slashing — matches Ethereum.
- **Epoch-boundary orchestration** — single deterministic sequence that rotates participation, computes flag deltas, updates inactivity scores, applies correlation penalties, and finalises expired slashes.
- **Genesis / initialisation policy** — explicit initial state + parameters.
- **Reward routing** — pay-to-puzzle-hash for whistleblower + proposer + appellant awards.
- **Bond escrow**`BondEscrow` trait; escrowed mojos held against reporter/appellant validator stake, released or forfeited per adjudication.
- **Reorg handling**`rewind_on_reorg(depth)` for participation + pending slashes + slashing protection.
- **Slashing protection**`SlashingProtection` validator-local JSON watermarks with surround-vote self-check.
- **REMARK wires + admission + mempool policy** for evidence and appeal.
- **Constants, error types, serialization.**

The crate does **not** own:

- **Block format**`dig-block`. Consumed.
- **Validator set + stake + effective-balance math + activation/exit queues**`dig-consensus`. Consumed via `ValidatorView` / `EffectiveBalanceView`.
- **Bond escrow storage**`dig-collateral` (or `dig-bond-escrow`). Consumed via `BondEscrow`.
- **Collateral manager**`dig-collateral`. Consumed via `CollateralSlasher`.
- **Fork choice / justification / finalisation**`dig-consensus`. Consumed via `JustificationView` / `ProposerView`.
- **Block re-execution engine** (used by invalid-block appeal oracle) — `dig-block` / `dig-clvm`. Consumed via `InvalidBlockOracle`.
- **Epoch arithmetic + lookback constants**`dig-epoch`. Re-exported.
- **Network id**`dig-constants`. Injected.
- **Attestation gossip + aggregation + inclusion**`dig-gossip` / `dig-consensus`. Tracker is told when an attestation is included; it does not observe gossip.
- **Mempool storage**`dig-mempool`.
- **Reward account storage**`dig-consensus` (or reward-distribution crate). Consumed via `RewardClawback` + `RewardPayout` traits.
- **CLVM execution**`dig-clvm`. `run_puzzle` dev-dep only.
- **DFSP / storage-provider slashing** — out of scope.

**Hard boundary:** every external input (pubkeys, balances, current epoch, network id, justification view, proposer view, bond escrow) is injected through traits/parameters. No database, no network, no CLVM in production. Policy is protocol law — constants, not configuration.

### 1.1 Design Principles

- **Validator-only scope.**
- **Four discrete slashable offenses.** All cryptographically provable. Inactivity is continuous accounting, not a slashable offense — matches Ethereum.
- **Ethereum-parity economics + shapes; DIG mechanics.** Weights, quotients, inactivity math match Ethereum Altair/Bellatrix. Block times and epoch sizes follow DIG (`BLOCKS_PER_EPOCH = 32` at 3 s/block → 96 s/epoch).
- **Optimistic slashing with fraud-proof appeal.** Slash is debited at evidence inclusion but reversible during an 8-epoch challenge window. If no winning appeal, slash finalises + correlation penalty applies + exit lock starts.
- **Appeals are deterministic fraud proofs.** They prove that the original evidence fails a verifier precondition — not that slashing policy was wrong.
- **Symmetric bonds.** Reporter escrows `REPORTER_BOND_MOJOS`; appellant escrows `APPELLANT_BOND_MOJOS`. Losing party forfeits; winning party receives 50%, 50% burned.
- **Single base-reward formula drives all participation economics.** `base_reward = effective_balance * BASE_REWARD_FACTOR / isqrt(total_active_balance)`.
- **Deterministic and pure.** Every verifier is a function of inputs. No I/O, no wall-clock, no RNG.
- **One source of truth per constant.** Lookback from `dig-epoch`. Block format from `dig-block`. BLS from `chia-bls`. No re-derivations.
- **Validator-local vs consensus-global are separate.** `SlashingProtection` is per-validator JSON; `SlashingManager` is consensus-global.

### 1.2 Crate Dependencies

| Crate | Version | Purpose |
|-------|---------|---------|
| `dig-block` | 0.1 | `L2BlockHeader`, `block_signing_message`, `beacon_block_header_signing_root`, `attestation_data_signing_root`. |
| `dig-epoch` | 0.1 | `SLASH_LOOKBACK_EPOCHS`, `CORRELATION_WINDOW_EPOCHS`, `BLOCKS_PER_EPOCH`, `L2_BLOCK_TIME_MS`, height↔epoch helpers. |
| `dig-constants` | 0.1 | `NetworkConstants`. |
| `chia-protocol` | 0.26 | `Bytes32`, `Coin`, `CoinSpend`. |
| `chia-bls` | 0.26 | `Signature`, `PublicKey`, `verify`, `aggregate`, `aggregate_verify`. |
| `chia-sha2` | 0.26 | `Sha256`. |
| `chia-sdk-types` | 0.30 | `MerkleTree` + `MerkleProof` for participation witness (reorg + appeal). `run_puzzle` dev-dep. |
| `clvm-utils` | 0.26 | `tree_hash` for REMARK puzzle-hash derivation. |
| `clvmr` | 0.11 | Dev-dep. |
| `serde`, `serde_json`, `serde_bytes`, `bincode` || Serialization. |
| `thiserror`, `hex`, `tracing` || Utility. |
| `parking_lot` || Optional `threadsafe`. |
| `num-integer` || `Roots::sqrt` for base-reward. |

### 1.3 Design Decisions

| # | Decision | Rationale |
|---|----------|-----------|
| 1 | Four discrete offenses (Proposer/Invalid-Block/Attester-Double-Vote/Attester-Surround-Vote) | Fully covered by cryptographic evidence. Matches Ethereum except sync-committee-slashing (no sync committee in DIG). |
| 2 | Inactivity is NOT an offense | Continuous per-epoch accounting. Penalties debited each epoch from `InactivityScoreTracker`, not via `SlashingManager`. Matches Ethereum's treatment of inactivity leak. Simplifies appeal system (no fraud proof for continuous accounting). |
| 3 | No sync committee | DIG does not ship sync committees. `WEIGHT_DENOMINATOR = 64` retained for Ethereum parity; 2 units unassigned. Attester max = 54/64 × base_reward. |
| 4 | Optimistic slashing with 8-epoch appeal window | Balances finality (~12.8 min at 32×3 s/epoch) against operator-mistake / evidence-forgery risk. |
| 5 | Appeals are fraud proofs | Deterministic, cannot be overturned by governance. |
| 6 | `BondEscrow` trait abstracts coin movement | `dig-slashing` does not own escrow storage; escrow lives in `dig-collateral` or a dedicated crate. |
| 7 | `RewardPayout` + `RewardClawback` traits abstract reward routing | Per-principal pay-to-puzzle-hash coins created by the consensus layer; clawback on sustained appeal debits that account before falling back to reporter bond. |
| 8 | `ProposerView` trait identifies block proposer at slot | Needed to route proposer-inclusion rewards and, at evidence-inclusion time, identify the block proposer for the whistleblower-proposer-reward leg. |
| 9 | `reporter_index ≠ accused_index` enforced at admission | Prevents self-slashing-for-profit (validator reports self → collects wb reward). |
| 10 | Explicit epoch-boundary ordering | (a) rotate participation, (b) compute flag deltas, (c) update inactivity scores, (d) inactivity penalties, (e) finalise expired slashes (incl. correlation). Fixed sequence; deterministic; testable. |
| 11 | Reorg handling in main body | Participation flags and pending slashes both sensitive to fork-choice reorgs. Explicit `rewind_on_reorg(depth)` on every stateful component. |
| 12 | `slash_absolute(mojos)` is the canonical API; legacy `slash(percentage)` deprecated | Historical l2_driver code uses percentages; Ethereum-parity math uses absolute mojos. `slash_absolute` is primary; `slash(pct)` shim retained for migration only. |
| 13 | Genesis initialisation fully specified | Prevents surprise during bootstrap. Empty `processed`, empty `PendingSlashBook`, zero inactivity scores, zero participation flags, `current_epoch = 0`. |
| 14 | Maximal reuse of Chia + DIG crates | No reimplementations of BLS, SHA-256, Merkle, signing messages, epoch constants. |

## 2. Constants

### 2.1 Penalty Base Rates (BPS)

```rust
pub const EQUIVOCATION_BASE_BPS: u16 = 500;
pub const INVALID_BLOCK_BASE_BPS: u16 = 300;
pub const ATTESTATION_BASE_BPS: u16 = 100;
pub const MAX_PENALTY_BPS: u16 = 1_000;
pub const BPS_DENOMINATOR: u64 = 10_000;
```

### 2.2 Ethereum-Parity Slashing Quotients

```rust
pub const MIN_SLASHING_PENALTY_QUOTIENT: u64 = 32;
pub const PROPORTIONAL_SLASHING_MULTIPLIER: u64 = 3;
pub use dig_epoch::CORRELATION_WINDOW_EPOCHS;
pub const SLASH_LOCK_EPOCHS: u64 = 100;
```

### 2.3 Reward Economics

```rust
pub const BASE_REWARD_FACTOR: u64 = 64;

/// WEIGHT_DENOMINATOR = 64, Ethereum-parity. 2 units are reserved (no sync committee
/// in DIG). Assigned weights sum to 62; max attester reward = 54/64 × base_reward.
pub const TIMELY_SOURCE_WEIGHT: u64 = 14;
pub const TIMELY_TARGET_WEIGHT: u64 = 26;
pub const TIMELY_HEAD_WEIGHT:   u64 = 14;
pub const PROPOSER_WEIGHT:      u64 = 8;
pub const WEIGHT_DENOMINATOR:   u64 = 64;

pub const WHISTLEBLOWER_REWARD_QUOTIENT: u64 = 512;
pub const PROPOSER_REWARD_QUOTIENT: u64 = 8;
```

### 2.4 Inactivity

```rust
pub const MIN_EPOCHS_TO_INACTIVITY_PENALTY: u64 = 4;
pub const INACTIVITY_SCORE_BIAS: u64 = 4;
pub const INACTIVITY_SCORE_RECOVERY_RATE: u64 = 16;
pub const INACTIVITY_PENALTY_QUOTIENT: u64 = 16_777_216;
```

### 2.5 Attestation Timeliness

```rust
pub const MIN_ATTESTATION_INCLUSION_DELAY: u64 = 1;
pub const TIMELY_SOURCE_MAX_DELAY_SLOTS: u64 = 5;   // integer_sqrt(32)
pub const TIMELY_TARGET_MAX_DELAY_SLOTS: u64 = 32;  // SLOTS_PER_EPOCH
pub const TIMELY_HEAD_MAX_DELAY_SLOTS: u64 = 1;
```

### 2.6 Appeal System

```rust
pub const SLASH_APPEAL_WINDOW_EPOCHS: u64 = 8;
pub const REPORTER_BOND_MOJOS: u64 = MIN_EFFECTIVE_BALANCE / 64;
pub const APPELLANT_BOND_MOJOS: u64 = MIN_EFFECTIVE_BALANCE / 64;
pub const BOND_AWARD_TO_WINNER_BPS: u16 = 5_000;  // 50%
pub const MAX_PENDING_SLASHES: usize = 4_096;
pub const MAX_APPEAL_ATTEMPTS_PER_SLASH: usize = 4;
pub const MAX_APPEAL_PAYLOAD_BYTES: usize = 131_072;  // 128 KiB
```

### 2.7 Lookback & Validator Parameters

```rust
pub use dig_epoch::SLASH_LOOKBACK_EPOCHS;
pub use dig_consensus::MIN_VALIDATOR_COLLATERAL as MIN_EFFECTIVE_BALANCE;
```

### 2.8 Block Admission

```rust
pub const MAX_SLASH_PROPOSALS_PER_BLOCK: usize = 64;
pub const MAX_SLASH_PROPOSAL_PAYLOAD_BYTES: usize = 65_536;
pub const MAX_APPEALS_PER_BLOCK: usize = 64;
pub const MAX_VALIDATORS_PER_COMMITTEE: usize = 2_048;
pub const MAX_ATTESTATIONS_PER_BLOCK: usize = 128;
```

### 2.9 Flag Bit Indices

```rust
pub const TIMELY_SOURCE_FLAG_INDEX: u8 = 0;
pub const TIMELY_TARGET_FLAG_INDEX: u8 = 1;
pub const TIMELY_HEAD_FLAG_INDEX:   u8 = 2;
```

### 2.10 Domain Separation Tags + BLS Widths

```rust
pub const DOMAIN_SLASHING_EVIDENCE:       &[u8] = b"DIG_SLASHING_EVIDENCE_V1";
pub const DOMAIN_SLASH_APPEAL:            &[u8] = b"DIG_SLASH_APPEAL_V1";
pub const DOMAIN_BEACON_PROPOSER:         &[u8] = b"DIG_BEACON_PROPOSER_V1";
pub const DOMAIN_BEACON_ATTESTER:         &[u8] = b"DIG_BEACON_ATTESTER_V1";
pub const DOMAIN_AGGREGATE_AND_PROOF:     &[u8] = b"DIG_AGGREGATE_AND_PROOF_V1";
pub const SLASH_EVIDENCE_REMARK_MAGIC_V1: &[u8] = b"DIG_SLASH_EVIDENCE_V1\0";
pub const SLASH_APPEAL_REMARK_MAGIC_V1:   &[u8] = b"DIG_SLASH_APPEAL_V1\0";

pub const BLS_SIGNATURE_SIZE:  usize = 96;
pub const BLS_PUBLIC_KEY_SIZE: usize = 48;
```

## 3. Data Model

### 3.1 Primitive Types

| Type | Source | Usage |
|------|--------|-------|
| `Bytes32` | `chia-protocol` | Every 32-byte hash. |
| `Signature` | `chia-bls` | Proposer/attester sigs. |
| `PublicKey` | `chia-bls` | Validator pubkeys. |
| `L2BlockHeader` | `dig-block` | Decoded from `ProposerSlashing` / `InvalidBlockProof`. |

### 3.2 OffenseType

```rust
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum OffenseType {
    ProposerEquivocation,
    InvalidBlock,
    AttesterDoubleVote,
    AttesterSurroundVote,
}

impl OffenseType {
    pub fn base_penalty_bps(&self) -> u16;
    pub fn name(&self) -> &'static str;
    pub fn description(&self) -> &'static str;
}
```

| Variant | `base_penalty_bps()` |
|---------|---------------------|
| `ProposerEquivocation` | `EQUIVOCATION_BASE_BPS` (500) |
| `InvalidBlock` | `INVALID_BLOCK_BASE_BPS` (300) |
| `AttesterDoubleVote` | `ATTESTATION_BASE_BPS` (100) |
| `AttesterSurroundVote` | `ATTESTATION_BASE_BPS` (100) |

### 3.3 Checkpoint / AttestationData / IndexedAttestation

```rust
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Checkpoint { pub epoch: u64, pub root: Bytes32 }

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct AttestationData {
    pub slot: u64,
    pub index: u64,                  // committee index
    pub beacon_block_root: Bytes32,  // head vote
    pub source: Checkpoint,          // FFG source
    pub target: Checkpoint,          // FFG target
}
impl AttestationData {
    pub fn signing_root(&self, network_id: &Bytes32) -> Bytes32;
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct IndexedAttestation {
    pub attesting_indices: Vec<u32>,  // strictly ascending, no duplicates
    pub data: AttestationData,
    #[serde(with = "serde_bytes")]
    pub signature: Vec<u8>,           // 96 bytes G2 aggregate
}
impl IndexedAttestation {
    pub fn validate_structure(&self) -> Result<(), SlashingError>;
    pub fn verify_signature(&self, pks: &dyn PublicKeyLookup, nid: &Bytes32) -> Result<(), SlashingError>;
}
```

Double-vote predicate: `a.target.epoch == b.target.epoch && a.data != b.data`.
Surround-vote predicate: `a.source.epoch < b.source.epoch && a.target.epoch > b.target.epoch` (or mirror).

### 3.4 Evidence Payloads

```rust
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SignedBlockHeader {
    pub message: L2BlockHeader,
    #[serde(with = "serde_bytes")]
    pub signature: Vec<u8>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProposerSlashing {
    pub signed_header_a: SignedBlockHeader,
    pub signed_header_b: SignedBlockHeader,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AttesterSlashing {
    pub attestation_a: IndexedAttestation,
    pub attestation_b: IndexedAttestation,
}
impl AttesterSlashing {
    pub fn slashable_indices(&self) -> Vec<u32>;
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum InvalidBlockReason {
    BadStateRoot, BadParentRoot, BadTimestamp, BadProposerIndex,
    TransactionExecutionFailure, OverweightBlock, DuplicateTransaction, Other,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InvalidBlockProof {
    pub signed_header: SignedBlockHeader,
    #[serde(with = "serde_bytes")]
    pub failure_witness: Vec<u8>,
    pub failure_reason: InvalidBlockReason,
}
```

### 3.5 SlashingEvidence (envelope)

```rust
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SlashingEvidence {
    pub offense_type: OffenseType,
    pub reporter_validator_index: u32,
    pub reporter_puzzle_hash: Bytes32,
    pub epoch: u64,
    pub payload: SlashingEvidencePayload,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SlashingEvidencePayload {
    Proposer(ProposerSlashing),
    Attester(AttesterSlashing),
    InvalidBlock(InvalidBlockProof),
}

impl SlashingEvidence {
    pub fn hash(&self) -> Bytes32;
    pub fn slashable_validators(&self) -> Vec<u32>;
}
```

### 3.6 Appeal Payloads (Fraud Proofs)

#### ProposerSlashingAppeal

```rust
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProposerSlashingAppeal {
    pub ground: ProposerAppealGround,
    #[serde(with = "serde_bytes")]
    pub witness: Vec<u8>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum ProposerAppealGround {
    HeadersIdentical,
    ProposerIndexMismatch,
    SignatureAInvalid,
    SignatureBInvalid,
    SlotMismatch,
    ValidatorNotActiveAtEpoch,
}
```

#### AttesterSlashingAppeal

```rust
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AttesterSlashingAppeal {
    pub ground: AttesterAppealGround,
    #[serde(with = "serde_bytes")]
    pub witness: Vec<u8>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum AttesterAppealGround {
    AttestationsIdentical,
    NotSlashableByPredicate,
    EmptyIntersection,
    SignatureAInvalid,
    SignatureBInvalid,
    InvalidIndexedAttestationStructure,
    ValidatorNotInIntersection { validator_index: u32 },
}
```

#### InvalidBlockAppeal

```rust
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InvalidBlockAppeal {
    pub ground: InvalidBlockAppealGround,
    #[serde(with = "serde_bytes")]
    pub witness: Vec<u8>,   // block body + pre-state + parent witness
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum InvalidBlockAppealGround {
    BlockActuallyValid,
    ProposerSignatureInvalid,
    FailureReasonMismatch,
    EvidenceEpochMismatch,
}
```

### 3.7 SlashAppeal (envelope)

```rust
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SlashAppeal {
    pub evidence_hash: Bytes32,
    pub appellant_index: u32,
    pub appellant_puzzle_hash: Bytes32,
    pub filed_epoch: u64,
    pub payload: SlashAppealPayload,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum SlashAppealPayload {
    Proposer(ProposerSlashingAppeal),
    Attester(AttesterSlashingAppeal),
    InvalidBlock(InvalidBlockAppeal),
}

impl SlashAppeal {
    pub fn hash(&self) -> Bytes32;
}
```

### 3.8 Pending Slash Lifecycle

```rust
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum PendingSlashStatus {
    Accepted,
    ChallengeOpen { first_appeal_filed_epoch: u64, appeal_count: u8 },
    Reverted  { winning_appeal_hash: Bytes32, reverted_at_epoch: u64 },
    Finalised { finalised_at_epoch: u64 },
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PendingSlash {
    pub evidence_hash: Bytes32,
    pub evidence: SlashingEvidence,
    pub verified: VerifiedEvidence,
    pub status: PendingSlashStatus,
    pub submitted_at_epoch: u64,
    pub window_expires_at_epoch: u64,  // submitted_at + SLASH_APPEAL_WINDOW_EPOCHS
    pub base_slash_per_validator: Vec<PerValidatorSlash>,
    pub reporter_bond_mojos: u64,
    pub appeal_history: Vec<AppealAttempt>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AppealAttempt {
    pub appeal_hash: Bytes32,
    pub appellant_index: u32,
    pub filed_epoch: u64,
    pub outcome: AppealOutcome,
    pub bond_mojos: u64,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum AppealOutcome {
    Won,
    Lost { reason_hash: Bytes32 },
    Pending,
}
```

### 3.9 Results

```rust
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct VerifiedEvidence {
    pub offense_type: OffenseType,
    pub slashable_validator_indices: Vec<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SlashingResult {
    pub per_validator: Vec<PerValidatorSlash>,
    pub whistleblower_reward: u64,
    pub proposer_reward: u64,
    pub burn_amount: u64,
    pub reporter_bond_escrowed: u64,
    pub pending_slash_hash: Bytes32,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PerValidatorSlash {
    pub validator_index: u32,
    pub base_slash_amount: u64,
    pub collateral_slashed: u64,
    pub effective_balance_at_slash: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum AppealVerdict {
    Sustained { reason: AppealSustainReason },
    Rejected  { reason: AppealRejectReason },
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AppealAdjudicationResult {
    pub appeal_hash: Bytes32,
    pub evidence_hash: Bytes32,
    pub outcome: AppealOutcome,
    pub reverted_stake_mojos: Vec<(u32, u64)>,
    pub reverted_collateral_mojos: Vec<(u32, u64)>,
    pub clawback_shortfall: u64,
    pub reporter_bond_forfeited: u64,
    pub appellant_award_mojos: u64,
    pub reporter_penalty_mojos: u64,
    pub appellant_bond_forfeited: u64,
    pub reporter_award_mojos: u64,
    pub burn_amount: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FinalisationResult {
    pub evidence_hash: Bytes32,
    pub per_validator_correlation_penalty: Vec<(u32, u64)>,
    pub reporter_bond_returned: u64,
    pub exit_lock_until_epoch: u64,
}
```

### 3.10 Participation + Inactivity + Protection

```rust
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
pub struct ParticipationFlags(pub u8);
impl ParticipationFlags {
    pub fn is_source_timely(&self) -> bool;
    pub fn is_target_timely(&self) -> bool;
    pub fn is_head_timely(&self) -> bool;
    pub fn set(&mut self, flag_index: u8);
    pub fn has(&self, flag_index: u8) -> bool;
}

pub struct ParticipationTracker { /* see §8 */ }
pub struct InactivityScoreTracker { /* see §9 */ }

#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SlashingProtection {
    pub last_proposed_slot: u64,
    pub last_attested_source_epoch: u64,
    pub last_attested_target_epoch: u64,
    #[serde(default)]
    pub last_attested_block_hash: Option<String>,
    pub last_attested_height: u64,
    pub last_attested_epoch: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FlagDelta {
    pub validator_index: u32,
    pub reward: u64,
    pub penalty: u64,
}
```

## 4. Slashing Geometry & Economics

```
─── Discrete Slashable Offenses ────────────────────────────────────────────
Offense               | Base BPS | Ethereum quotient       | Appeal window
----------------------+----------+------------------------+------------------
ProposerEquivocation  |   500    | eff_bal / 32            | 8 epochs
InvalidBlock          |   300    | eff_bal / 32            | 8 epochs
AttesterDoubleVote    |   100    | eff_bal / 32            | 8 epochs
AttesterSurroundVote  |   100    | eff_bal / 32            | 8 epochs

base_slash = max(
    eff_bal * base_bps / 10_000,
    eff_bal / MIN_SLASHING_PENALTY_QUOTIENT
)

At finalisation (not on admission):
    correlation_penalty = eff_bal
        * min(total_slashed_in_window * 3, total_active_balance)
        / total_active_balance

On admission (reversible):
    wb_reward  = eff_bal / 512
    prop_reward= wb_reward / 8
    burn       = base_slash - wb_reward - prop_reward
    reporter_bond escrowed (eff_bal / 64)
    base slash applied to validator

─── Participation (per validator per epoch, Ethereum Altair minus sync) ────
base_reward = effective_balance * 64 / integer_sqrt(total_active_balance)

TIMELY_SOURCE (14/64): reward on hit,   penalty on miss
TIMELY_TARGET (26/64): reward on hit,   penalty on miss
TIMELY_HEAD   (14/64): reward on hit,   NO penalty on miss
Proposer inclusion  : base_reward * 8 / (64 - 8)

In finality stall: rewards = 0, penalties still debit.

─── Inactivity Accounting (continuous, not a slashing event) ───────────────
On target miss + stall: score += 4
Otherwise (in stall)  : score -= min(1, score)
Out of stall          : score -= min(16, score) (global recovery)
Per-epoch penalty     : eff_bal * score / 16_777_216  (ONLY during stall)

─── Appeal Economics ───────────────────────────────────────────────────────
Reporter bond  : eff_bal / 64   escrowed at evidence admission
Appellant bond : eff_bal / 64   escrowed at appeal admission

On appeal WIN:
    Revert   : base slash + collateral slash (per slashed validator)
    Clawback : wb_reward + prop_reward from their reward accounts
    Forfeit  : reporter bond
    Award    : appellant_award = forfeited * 5_000 / 10_000
    Burn     : forfeited - appellant_award + clawback_shortfall
    Penalise : reporter slashed (InvalidBlock base) for filing false evidence

On appeal LOSS:
    Forfeit  : appellant bond
    Award    : reporter_award = forfeited * 5_000 / 10_000
    Burn     : forfeited - reporter_award
    Slash persists; further appeals accepted up to MAX_APPEAL_ATTEMPTS_PER_SLASH.

On window EXPIRY (no winning appeal):
    Status -> Finalised
    Correlation penalty applied
    Exit lock starts (SLASH_LOCK_EPOCHS)
    Reporter bond returned in full
```

## 5. Evidence Verification

### 5.1 Dispatcher

```rust
pub fn verify_evidence(
    evidence: &SlashingEvidence,
    validator_view: &dyn ValidatorView,
    network_id: &Bytes32,
    current_epoch: u64,
) -> Result<VerifiedEvidence, SlashingError>;
```

Preconditions before dispatch:

1. `evidence.epoch + SLASH_LOOKBACK_EPOCHS >= current_epoch` — else `OffenseTooOld`.
2. `evidence.reporter_validator_index` is live and active — else `ReporterNotRegistered`.
3. `evidence.reporter_validator_index``evidence.slashable_validators()` — else `ReporterIsAccused`.

Then dispatch per payload.

### 5.2 Proposer Slashing

1. `signed_header_a.message.slot == signed_header_b.message.slot`.
2. `signed_header_a.message.proposer_index == signed_header_b.message.proposer_index`.
3. `signed_header_a.message.hash() != signed_header_b.message.hash()`.
4. Both signatures parse (96 bytes G2).
5. Accused validator exists, is not already slashed, is active at `header.epoch`.
6. Both sigs verify via `chia_bls::verify(sig, pk, block_signing_message(network_id, header.epoch, &header.hash(), header.proposer_index))`.

### 5.3 Attester Slashing

1. Both `IndexedAttestation::validate_structure()` succeed (ascending indices, no dup, len in [1, `MAX_VALIDATORS_PER_COMMITTEE`], sig 96 bytes).
2. `attestation_a != attestation_b`.
3. **Double-vote** OR **surround-vote** predicate holds.
4. Both `IndexedAttestation::verify_signature` succeed via `chia_bls::aggregate_verify`.
5. `slashable_indices = a.attesting_indices ∩ b.attesting_indices` non-empty.
6. Every slashable index is a live validator, not already slashed.

### 5.4 Invalid-Block

1. Signature verifies over `block_signing_message(...)`.
2. `header.epoch == evidence.epoch`.
3. `header.proposer_index` identifies a live validator.
4. `failure_witness.len()``[1, MAX_SLASH_PROPOSAL_PAYLOAD_BYTES]`.
5. Optional `InvalidBlockOracle::verify_failure(header, witness, reason)` — if oracle is supplied, it must return `Ok`. Bootstrap path: no oracle required; caller defers to the challenge window for correctness.

### 5.5 Mempool-Admission Verification

```rust
pub fn verify_evidence_for_inclusion(
    evidence: &SlashingEvidence,
    validator_view: &dyn ValidatorView,
    network_id: &Bytes32,
    current_epoch: u64,
) -> Result<VerifiedEvidence, SlashingError>;
```

Identical to §5.1 minus state mutation; called by mempool + block-admission pipelines.

## 6. Appeal Verification

### 6.1 Dispatcher

```rust
pub fn verify_appeal(
    appeal: &SlashAppeal,
    pending: &PendingSlash,
    validator_view: &dyn ValidatorView,
    network_id: &Bytes32,
    current_epoch: u64,
    justification: &dyn JustificationView,
    invalid_block_oracle: Option<&dyn InvalidBlockOracle>,
) -> Result<AppealVerdict, AppealError>;
```

Preconditions (any failure → `AppealError`):

1. `appeal.evidence_hash == pending.evidence_hash`.
2. `pending.status ∈ { Accepted, ChallengeOpen }` — not `Reverted` / `Finalised`.
3. `pending.submitted_at_epoch <= appeal.filed_epoch <= pending.window_expires_at_epoch`.
4. `appeal.filed_epoch <= current_epoch`.
5. Appellant is a live, active validator.
6. `appeal.payload` variant matches `pending.evidence.payload` variant.
7. Serialized payload ≤ `MAX_APPEAL_PAYLOAD_BYTES`.
8. Not a byte-duplicate of any prior `AppealAttempt`.
9. `pending.appeal_history.len() < MAX_APPEAL_ATTEMPTS_PER_SLASH`.

### 6.2 Proposer Appeal Grounds

Per `ProposerAppealGround`:

- **HeadersIdentical** — assert `signed_header_a.message == signed_header_b.message` bytewise. Sustained iff true.
- **ProposerIndexMismatch**`header_a.proposer_index != header_b.proposer_index`. Sustained iff true.
- **SignatureAInvalid** — BLS verify of sig_a over `block_signing_message`. Sustained iff verify returns false.
- **SignatureBInvalid** — same for sig_b.
- **SlotMismatch**`header_a.slot != header_b.slot`. Sustained iff true.
- **ValidatorNotActiveAtEpoch**`validator.is_active_at_epoch(header.epoch)` is false. Witness carries `ActivationRange { activation_epoch, exit_epoch }` + proof root; verifier consults `validator_view`. Sustained iff not active.

### 6.3 Attester Appeal Grounds

- **AttestationsIdentical** — byte-equal.
- **NotSlashableByPredicate** — neither double-vote nor surround-vote predicate holds.
- **EmptyIntersection** — intersection empty.
- **SignatureAInvalid / SignatureBInvalid** — aggregate verify fails.
- **InvalidIndexedAttestationStructure**`validate_structure` errors on either side.
- **ValidatorNotInIntersection { validator_index }** — specified index not in intersection.

### 6.4 Invalid-Block Appeal Grounds

- **BlockActuallyValid** — requires `InvalidBlockOracle`. Witness carries block body + pre-state + parent witness. Oracle `re_execute` returns `ExecutionOutcome::Valid`. Sustained iff so. Without oracle: `MissingOracle` error (cannot adjudicate).
- **ProposerSignatureInvalid** — BLS verify over `block_signing_message` returns false.
- **FailureReasonMismatch** — oracle `re_execute` returns `Invalid(actual_reason)` where `actual_reason != evidence.failure_reason`.
- **EvidenceEpochMismatch**`header.epoch != evidence.epoch`.

### 6.5 Adjudicator

```rust
pub struct AppealAdjudicator;

impl AppealAdjudicator {
    pub fn adjudicate(
        &mut self,
        appeal: &SlashAppeal,
        verdict: &AppealVerdict,
        pending: &mut PendingSlash,
        validator_set: &mut dyn ValidatorView,
        effective_balances: &dyn EffectiveBalanceView,
        collateral: Option<&mut dyn CollateralSlasher>,
        bond_escrow: &mut dyn BondEscrow,
        reward_payout: &mut dyn RewardPayout,
        reward_clawback: &mut dyn RewardClawback,
        current_epoch: u64,
    ) -> AppealAdjudicationResult;
}
```

On **Sustained**:

1. For each `PerValidatorSlash` in `pending.base_slash_per_validator`:
   - `validator.credit_stake(base_slash_amount)`.
   - If collateral present: `collateral.credit(idx, collateral_slashed)`.
   - `validator.restore_status()`.
2. `reward_clawback.claw_back(reporter_ph, wb_reward)` and `claw_back(proposer_ph, prop_reward)`.
3. `shortfall = (wb_reward + prop_reward) - total_clawed_back`.
4. `bond_escrow.forfeit(reporter_idx, REPORTER_BOND_MOJOS, BondTag::Reporter(evidence_hash))` → forfeited.
5. `appellant_award = forfeited * BOND_AWARD_TO_WINNER_BPS / BPS_DENOMINATOR`.
6. `burn = forfeited - appellant_award + shortfall`. (Shortfall absorbed by burning from forfeited bond; no protocol debt unless shortfall > forfeited, which is flagged in telemetry.)
7. `reward_payout.pay(appellant_ph, appellant_award)`.
8. Reporter penalty: `base = max(eff_bal * INVALID_BLOCK_BASE_BPS / 10_000, eff_bal / 32)`. `validator.slash_absolute(base, current_epoch)`. Record into `slashed_in_window`.
9. `pending.status = Reverted { winning_appeal_hash, reverted_at_epoch: current_epoch }`.
10. Append `AppealAttempt { outcome: Won }`.

On **Rejected**:

1. `bond_escrow.forfeit(appellant_idx, APPELLANT_BOND_MOJOS, BondTag::Appellant(appeal_hash))` → forfeited.
2. `reporter_award = forfeited * BOND_AWARD_TO_WINNER_BPS / BPS_DENOMINATOR`.
3. `burn = forfeited - reporter_award`.
4. `reward_payout.pay(reporter_ph, reporter_award)`.
5. `pending.status = ChallengeOpen { first_appeal_filed_epoch: min(existing, filed), appeal_count: n + 1 }`.
6. Append `AppealAttempt { outcome: Lost { reason_hash } }`.

## 7. Slashing State Machine

### 7.1 PendingSlashBook

```rust
pub struct PendingSlashBook {
    pending: HashMap<Bytes32, PendingSlash>,
    by_window_expiry: BTreeMap<u64, Vec<Bytes32>>,
    capacity: usize,
}

impl PendingSlashBook {
    pub fn new(capacity: usize) -> Self;
    pub fn insert(&mut self, record: PendingSlash) -> Result<(), SlashingError>;
    pub fn get(&self, h: &Bytes32) -> Option<&PendingSlash>;
    pub fn get_mut(&mut self, h: &Bytes32) -> Option<&mut PendingSlash>;
    pub fn remove(&mut self, h: &Bytes32) -> Option<PendingSlash>;
    pub fn expired_by(&self, current_epoch: u64) -> Vec<Bytes32>;
    pub fn len(&self) -> usize;
}
```

### 7.2 SlashingManager

```rust
pub struct SlashingManager {
    book: PendingSlashBook,
    processed: HashMap<Bytes32, u64>,
    slashed_in_window: BTreeMap<(u64, u32), u64>,  // (epoch, idx) → eff_bal_at_slash
    current_epoch: u64,
}

impl SlashingManager {
    /// Genesis initialisation: empty book, empty processed, empty window, epoch=0.
    pub fn new(current_epoch: u64) -> Self;

    pub fn set_epoch(&mut self, epoch: u64);

    pub fn submit_evidence(
        &mut self,
        evidence: SlashingEvidence,
        validator_set: &mut dyn ValidatorView,
        effective_balances: &dyn EffectiveBalanceView,
        collateral: Option<&mut dyn CollateralSlasher>,
        bond_escrow: &mut dyn BondEscrow,
        reward_payout: &mut dyn RewardPayout,
        proposer: &dyn ProposerView,
        network_id: &Bytes32,
    ) -> Result<SlashingResult, SlashingError>;

    pub fn submit_appeal(
        &mut self,
        appeal: SlashAppeal,
        validator_set: &mut dyn ValidatorView,
        effective_balances: &dyn EffectiveBalanceView,
        collateral: Option<&mut dyn CollateralSlasher>,
        bond_escrow: &mut dyn BondEscrow,
        reward_payout: &mut dyn RewardPayout,
        reward_clawback: &mut dyn RewardClawback,
        network_id: &Bytes32,
        justification: &dyn JustificationView,
        invalid_block_oracle: Option<&dyn InvalidBlockOracle>,
    ) -> Result<AppealAdjudicationResult, AppealError>;

    /// Called once at each epoch boundary, AFTER participation rotation and
    /// BEFORE new-epoch block production. Transitions all expired PendingSlashes
    /// to Finalised; applies correlation penalty; returns reporter bonds;
    /// starts exit locks.
    pub fn finalise_expired_slashes(
        &mut self,
        validator_set: &mut dyn ValidatorView,
        effective_balances: &dyn EffectiveBalanceView,
        bond_escrow: &mut dyn BondEscrow,
        reward_payout: &mut dyn RewardPayout,
        total_active_balance: u64,
    ) -> Vec<FinalisationResult>;

    pub fn is_slashed(&self, idx: u32, vs: &dyn ValidatorView) -> bool;
    pub fn is_processed(&self, h: &Bytes32) -> bool;
    pub fn pending(&self, h: &Bytes32) -> Option<&PendingSlash>;
    pub fn prune(&mut self, before_epoch: u64);

    /// Reorg: roll back all pending slashes submitted at epochs > new_tip_epoch.
    /// Caller (consensus) handles restoring validator stake via the same path
    /// as a Sustained appeal (minus reporter penalty).
    pub fn rewind_on_reorg(
        &mut self,
        new_tip_epoch: u64,
        validator_set: &mut dyn ValidatorView,
        collateral: Option<&mut dyn CollateralSlasher>,
        bond_escrow: &mut dyn BondEscrow,
    ) -> Vec<Bytes32>;
}
```

### 7.3 submit_evidence Pipeline

1. `hash = evidence.hash()`. If in `processed``AlreadySlashed`.
2. `verified = verify_evidence(...)`.
3. Book at capacity? → `PendingBookFull`.
4. `bond_escrow.lock(reporter_idx, REPORTER_BOND_MOJOS, BondTag::Reporter(hash))`. If fails → `BondLockFailed`.
5. For each `idx` in `verified.slashable_validator_indices`:
   - Skip if validator missing or already slashed.
   - `eff_bal = effective_balances.get(idx)`.
   - `base_slash = max(eff_bal * base_bps / 10_000, eff_bal / 32)`.
   - `validator.slash_absolute(base_slash, current_epoch)`.
   - If collateral present: `collateral.slash(idx, base_slash, current_epoch)``coll_slashed`.
   - `slashed_in_window.insert((current_epoch, idx), eff_bal)`.
   - Push `PerValidatorSlash { base_slash_amount, collateral_slashed: coll_slashed, effective_balance_at_slash: eff_bal }`.
6. `total_base = Σ base_slash`. `total_eff = Σ eff_bal`.
7. `wb_reward = total_eff / WHISTLEBLOWER_REWARD_QUOTIENT`.
8. `prop_reward = wb_reward / PROPOSER_REWARD_QUOTIENT`.
9. `burn = total_base - wb_reward - prop_reward`.
10. `reward_payout.pay(reporter_puzzle_hash, wb_reward)`.
11. `block_proposer_idx = proposer.proposer_at_slot(current_slot)?`. `block_proposer_ph = validator_set.get(block_proposer_idx).puzzle_hash()`. `reward_payout.pay(block_proposer_ph, prop_reward)`.
12. `book.insert(PendingSlash { status: Accepted, submitted_at_epoch, window_expires_at_epoch: current_epoch + 8, ... })`.
13. `processed.insert(hash, current_epoch)`.
14. Return `SlashingResult`.

### 7.4 finalise_expired_slashes Pipeline

For each expired `evidence_hash` in `book.expired_by(current_epoch)`:

1. `pending = book.get_mut(hash)`.
2. If `pending.status == Reverted` or `Finalised` → skip (defensive).
3. Compute `cohort_sum = Σ eff_bal over slashed_in_window entries in [current_epoch - CORRELATION_WINDOW_EPOCHS, current_epoch]`.
4. For each `idx` in `pending.base_slash_per_validator`:
   - `eff_bal = effective_balances.get(idx)`.
   - `corr = eff_bal * min(cohort_sum * 3, total_active_balance) / total_active_balance`.
   - `validator.slash_absolute(corr, current_epoch)`.
   - Set `exit_eligible_epoch = current_epoch + SLASH_LOCK_EPOCHS` (handled by validator_set via `schedule_exit`).
5. `bond_escrow.release(reporter_idx, REPORTER_BOND_MOJOS, BondTag::Reporter(hash))` → return in full.
6. `pending.status = Finalised { finalised_at_epoch: current_epoch }`.
7. Emit `FinalisationResult`.

## 8. Attestation Participation & Rewards

### 8.1 Timeliness Classification

```rust
/// Given an attestation included at `inclusion_slot`, classify timeliness
/// against the canonical fork-choice view.
pub fn classify_timeliness(
    data: &AttestationData,
    inclusion_slot: u64,
    source_is_justified: bool,
    target_is_canonical: bool,
    head_is_canonical: bool,
) -> ParticipationFlags;
```

Rules:

- `delay = inclusion_slot - data.slot`.
- `TIMELY_SOURCE` set iff `delay ∈ [1, 5] && source_is_justified`.
- `TIMELY_TARGET` set iff `delay ∈ [1, 32] && target_is_canonical`.
- `TIMELY_HEAD` set iff `delay == 1 && head_is_canonical`.

### 8.2 ParticipationTracker

```rust
pub struct ParticipationTracker {
    previous_epoch: Vec<ParticipationFlags>,
    current_epoch:  Vec<ParticipationFlags>,
    current_epoch_number: u64,
}

impl ParticipationTracker {
    pub fn new(validator_count: usize, current_epoch: u64) -> Self;

    pub fn record_attestation(
        &mut self,
        data: &AttestationData,
        attesting_indices: &[u32],
        flags_for_all: ParticipationFlags,
    ) -> Result<(), ParticipationError>;

    pub fn rotate_epoch(&mut self, new_epoch: u64, validator_count: usize);

    pub fn previous_epoch_flags(&self, idx: u32) -> ParticipationFlags;
    pub fn current_epoch_flags(&self, idx: u32) -> ParticipationFlags;
    pub fn previous_epoch_all(&self) -> &[ParticipationFlags];

    /// Reorg: drop the last `depth` epochs of recorded flags.
    pub fn rewind_on_reorg(&mut self, depth: u64, validator_count: usize);
}
```

`record_attestation` validates `attesting_indices` (ascending, no dup, in range) then sets `flags_for_all` into `current_epoch[idx]` for each attester.

### 8.3 Base Reward + Flag Deltas

```rust
pub fn base_reward(effective_balance: u64, total_active_balance: u64) -> u64;

pub fn compute_flag_deltas(
    previous_epoch_participation: &[ParticipationFlags],
    effective_balances: &dyn EffectiveBalanceView,
    total_active_balance: u64,
    in_finality_stall: bool,
) -> Vec<FlagDelta>;
```

Per validator:

```
base  = base_reward(eff_bal, total_active_balance)
src_w = base * 14 / 64
tgt_w = base * 26 / 64
hd_w  = base * 14 / 64

if TIMELY_SOURCE set: reward += src_w else penalty += src_w
if TIMELY_TARGET set: reward += tgt_w else penalty += tgt_w
if TIMELY_HEAD   set: reward += hd_w  // head miss: no penalty

if in_finality_stall:
    reward = 0
```

Deltas are not automatically debited/credited — the caller (consensus) applies them to validator balances.

### 8.4 Proposer Inclusion Reward

```rust
pub fn proposer_inclusion_reward(attester_base_reward: u64) -> u64 {
    attester_base_reward * PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT)
    // = base * 8 / 56
}
```

Accumulated by the consensus layer and routed to the proposer of the first block that includes a given attestation.

## 9. Inactivity Accounting (Continuous)

### 9.1 Finality-Stall Detection

```rust
pub fn in_finality_stall(current_epoch: u64, finalized_epoch: u64) -> bool {
    current_epoch.saturating_sub(finalized_epoch) > MIN_EPOCHS_TO_INACTIVITY_PENALTY
}
```

### 9.2 Score Update

```rust
impl InactivityScoreTracker {
    pub fn new(validator_count: usize) -> Self;
    pub fn resize_for(&mut self, validator_count: usize);
    pub fn score(&self, idx: u32) -> u64;

    pub fn update_for_epoch(
        &mut self,
        previous_epoch_participation: &[ParticipationFlags],
        in_finality_stall: bool,
    ) {
        for (i, flags) in previous_epoch_participation.iter().enumerate() {
            let s = &mut self.scores[i];
            if flags.is_target_timely() {
                *s = s.saturating_sub(1);
            } else if in_finality_stall {
                *s = s.saturating_add(INACTIVITY_SCORE_BIAS);
            }
        }
        if !in_finality_stall {
            for s in &mut self.scores { *s = s.saturating_sub(INACTIVITY_SCORE_RECOVERY_RATE); }
        }
    }

    pub fn epoch_penalties(
        &self,
        effective_balances: &dyn EffectiveBalanceView,
        in_finality_stall: bool,
    ) -> Vec<(u32, u64)>;

    /// Reorg: drop `depth` epochs of score accumulation. Implemented as a
    /// shadow buffer of prior scores; rewind restores the snapshot.
    pub fn rewind_on_reorg(&mut self, depth: u64);
}
```

### 9.3 Per-Epoch Penalty

```
penalty = eff_bal * score / INACTIVITY_PENALTY_QUOTIENT   (only when in stall)
```

Non-zero penalties returned as `Vec<(validator_index, mojos)>`; consensus applies them to validator balances.

## 10. Epoch Boundary Orchestration

Consensus calls this sequence exactly once per epoch transition from `N` to `N+1`. Ordering is fixed and deterministic.

```rust
pub fn run_epoch_boundary(
    manager: &mut SlashingManager,
    participation: &mut ParticipationTracker,
    inactivity: &mut InactivityScoreTracker,
    validator_set: &mut dyn ValidatorView,
    effective_balances: &dyn EffectiveBalanceView,
    bond_escrow: &mut dyn BondEscrow,
    reward_payout: &mut dyn RewardPayout,
    justification: &dyn JustificationView,
    current_epoch_ending: u64,
    validator_count: usize,
    total_active_balance: u64,
) -> EpochBoundaryReport;
```

**Ordering:**

1. **Compute flag deltas** over `participation.previous_epoch_all()` with `in_finality_stall = in_finality_stall(current_epoch_ending, justification.finalized_checkpoint().epoch)`.
   → returned in `EpochBoundaryReport::flag_deltas` for consensus to apply.
2. **Update inactivity scores** over the same previous-epoch flags.
3. **Compute inactivity penalties** for the ending epoch (non-zero only if in stall).
   → returned in `EpochBoundaryReport::inactivity_penalties`.
4. **Finalise expired slashes** via `manager.finalise_expired_slashes(...)` (correlation penalty + reporter bond return + exit lock).
   → returned in `EpochBoundaryReport::finalisations`.
5. **Rotate participation tracker** to `current_epoch_ending + 1` with `validator_count`.
6. **Advance manager epoch** via `manager.set_epoch(current_epoch_ending + 1)`.
7. **Resize trackers** if `validator_count` changed (new activations).
8. **Prune old processed evidence and correlation-window entries** via `manager.prune(current_epoch_ending.saturating_sub(SLASH_LOOKBACK_EPOCHS + 1))`.

```rust
pub struct EpochBoundaryReport {
    pub flag_deltas: Vec<FlagDelta>,
    pub inactivity_penalties: Vec<(u32, u64)>,
    pub finalisations: Vec<FinalisationResult>,
    pub in_finality_stall: bool,
}
```

## 11. Genesis & Initialisation

```rust
pub struct GenesisParameters {
    pub genesis_epoch: u64,               // typically 0
    pub initial_validator_count: usize,
    pub network_id: Bytes32,
}

pub struct SlashingSystem {
    pub manager: SlashingManager,
    pub participation: ParticipationTracker,
    pub inactivity: InactivityScoreTracker,
}

impl SlashingSystem {
    pub fn genesis(params: &GenesisParameters) -> Self {
        Self {
            manager: SlashingManager::new(params.genesis_epoch),
            participation: ParticipationTracker::new(params.initial_validator_count, params.genesis_epoch),
            inactivity: InactivityScoreTracker::new(params.initial_validator_count),
        }
    }
}
```

At genesis:

- `processed` is empty; no evidence has been seen.
- `PendingSlashBook` is empty.
- `slashed_in_window` is empty.
- All validators have `ParticipationFlags(0)` for both previous and current epoch.
- All inactivity scores are 0.
- `in_finality_stall` returns false at `(0, 0)`.

## 12. Reward Routing & Bond Escrow

### 12.1 RewardPayout

```rust
pub trait RewardPayout {
    /// Create (or credit) a pay-to-puzzle-hash account for `principal_ph`
    /// with `amount_mojos`. Implementations (in consensus layer) create
    /// a reward coin or credit an existing account.
    fn pay(&mut self, principal_ph: Bytes32, amount_mojos: u64);
}
```

Paid principals:
- Reporter puzzle hash → `wb_reward` on evidence inclusion.
- Block-proposer puzzle hash → `prop_reward` on evidence inclusion.
- Appellant puzzle hash → `appellant_award` on sustained appeal.
- Reporter puzzle hash → `reporter_award` on rejected appeal.

### 12.2 RewardClawback

```rust
pub trait RewardClawback {
    /// Deduct up to `amount` from `principal_ph`'s reward account.
    /// Returns amount actually clawed back (may be < `amount` if already withdrawn).
    fn claw_back(&mut self, principal_ph: Bytes32, amount: u64) -> u64;
}
```

Called on sustained appeal to reverse the optimistic `wb_reward` + `prop_reward`. Shortfall is absorbed by the forfeited reporter bond; residue (if any) is burned and flagged.

### 12.3 BondEscrow

```rust
pub trait BondEscrow {
    fn lock(&mut self, principal_idx: u32, amount: u64, tag: BondTag) -> Result<(), BondError>;
    fn release(&mut self, principal_idx: u32, amount: u64, tag: BondTag) -> Result<(), BondError>;
    fn forfeit(&mut self, principal_idx: u32, amount: u64, tag: BondTag) -> Result<u64, BondError>;
    fn escrowed(&self, principal_idx: u32, tag: BondTag) -> u64;
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum BondTag {
    Reporter(Bytes32),   // evidence_hash
    Appellant(Bytes32),  // appeal_hash
}
```

Implementation (in `dig-collateral` or dedicated crate) holds escrowed mojos against the validator's stake. If `lock` is called while `stake_after_base_slash < amount`, it returns `InsufficientBalance`. For evidence admission this means the reporter must have enough stake to cover the bond; for appeals, the appellant must have enough.

## 13. Reorg Handling

A fork-choice reorg invalidates previously-recorded participation flags, pending slashes, and inactivity-score updates that happened on the replaced branch.

```rust
pub fn rewind_all_on_reorg(
    manager: &mut SlashingManager,
    participation: &mut ParticipationTracker,
    inactivity: &mut InactivityScoreTracker,
    protection: &mut SlashingProtection,
    validator_set: &mut dyn ValidatorView,
    collateral: Option<&mut dyn CollateralSlasher>,
    bond_escrow: &mut dyn BondEscrow,
    new_tip_epoch: u64,
    new_tip_slot: u64,
    validator_count: usize,
) -> ReorgReport;
```

Sequence:

1. `manager.rewind_on_reorg(new_tip_epoch, ...)` — pending slashes submitted at epochs > `new_tip_epoch` are dropped. For each: credit back base slash (like Sustained appeal, no reporter penalty), release reporter bond in full, remove from `processed` and `slashed_in_window`.
2. `participation.rewind_on_reorg(current_epoch - new_tip_epoch, validator_count)` — drop epochs > `new_tip_epoch`.
3. `inactivity.rewind_on_reorg(current_epoch - new_tip_epoch)` — restore prior score snapshot.
4. `protection.reconcile_with_chain_tip(new_tip_slot, new_tip_epoch)` — validator-local watermarks rewound.

```rust
pub struct ReorgReport {
    pub rewound_pending_slashes: Vec<Bytes32>,
    pub participation_epochs_dropped: u64,
    pub inactivity_epochs_dropped: u64,
    pub protection_rewound: bool,
}
```

**Snapshot retention:** Both `ParticipationTracker` and `InactivityScoreTracker` maintain a ring buffer of prior-epoch states of depth `CORRELATION_WINDOW_EPOCHS`. Reorgs deeper than that are a protocol-level failure; `ReorgTooDeep` error returned.

## 14. Slashing Protection (Validator-Local)

(As v0.3. Summary:)

- JSON watermarks on disk.
- `check_proposal_slot(slot) -> bool`: slot > `last_proposed_slot`.
- `check_attestation(source, target, hash) -> bool`: strictly monotone target; source not earlier than `last_attested_source_epoch`; surround-vote self-check; same (source, target) → same hash.
- `would_surround(source, target) -> bool`.
- `record_proposal(slot)`, `record_attestation(source, target, hash)`.
- `rewind_proposal_to_slot`, `rewind_attestation_to_epoch`, `reconcile_with_chain_tip`.
- Legacy JSON (no hash field) loads with `None`.

## 15. Traits

### 15.1 ValidatorView + ValidatorEntry

```rust
pub trait ValidatorView {
    fn get(&self, index: u32) -> Option<&dyn ValidatorEntry>;
    fn get_mut(&mut self, index: u32) -> Option<&mut dyn ValidatorEntry>;
    fn len(&self) -> usize;
}

pub trait ValidatorEntry {
    fn public_key(&self) -> &PublicKey;
    fn puzzle_hash(&self) -> Bytes32;
    fn effective_balance(&self) -> u64;
    fn is_slashed(&self) -> bool;
    fn activation_epoch(&self) -> u64;
    fn exit_epoch(&self) -> u64;
    fn is_active_at_epoch(&self, epoch: u64) -> bool;

    /// Canonical slashing API: debit by absolute mojos.
    fn slash_absolute(&mut self, amount_mojos: u64, epoch: u64) -> u64;

    /// Undo a prior slash_absolute on sustained appeal or reorg.
    fn credit_stake(&mut self, amount_mojos: u64) -> u64;

    /// Clear Slashed; restore Active. Returns true if status changed.
    fn restore_status(&mut self) -> bool;

    /// Schedule the exit lock after a finalised slash.
    fn schedule_exit(&mut self, exit_lock_until_epoch: u64);
}
```

### 15.2 EffectiveBalanceView, PublicKeyLookup, CollateralSlasher

```rust
pub trait EffectiveBalanceView {
    fn get(&self, index: u32) -> u64;
    fn total_active(&self) -> u64;
}

pub trait PublicKeyLookup {
    fn pubkey_of(&self, index: u32) -> Option<&PublicKey>;
}

pub trait CollateralSlasher {
    fn slash(&mut self, idx: u32, amount: u64, epoch: u64) -> Result<(u64, u64), CollateralError>;
    fn credit(&mut self, idx: u32, amount: u64) -> Result<u64, CollateralError>;
}
```

### 15.3 Fork-Choice & Block Oracles

```rust
pub trait JustificationView {
    fn current_justified_checkpoint(&self) -> Checkpoint;
    fn previous_justified_checkpoint(&self) -> Checkpoint;
    fn finalized_checkpoint(&self) -> Checkpoint;
    fn canonical_block_root_at_slot(&self, slot: u64) -> Option<Bytes32>;
    fn canonical_target_root_for_epoch(&self, epoch: u64) -> Option<Bytes32>;
}

pub trait ProposerView {
    fn proposer_at_slot(&self, slot: u64) -> Option<u32>;
    fn current_slot(&self) -> u64;
}

pub trait InvalidBlockOracle {
    fn verify_failure(
        &self, header: &L2BlockHeader, witness: &[u8], reason: InvalidBlockReason,
    ) -> Result<(), SlashingError> { Ok(()) }
    fn re_execute(
        &self, header: &L2BlockHeader, witness: &[u8],
    ) -> Result<ExecutionOutcome, SlashingError>;
}

pub enum ExecutionOutcome {
    Valid,
    Invalid(InvalidBlockReason),
}
```

### 15.4 Bonds + Rewards

(See §12.)

## 16. REMARK Admission

### 16.1 Evidence REMARK

```
wire = SLASH_EVIDENCE_REMARK_MAGIC_V1 || serde_json(SlashingEvidence)
```

APIs: `encode_slashing_evidence_remark_payload_v1`, `slashing_evidence_remark_puzzle_reveal_v1`, `slashing_evidence_remark_puzzle_hash_v1`, `build_slashing_evidence_remark_spend_bundle`, `parse_slashing_evidence_from_conditions`, `enforce_slashing_evidence_remark_admission`, `enforce_slashing_evidence_mempool_policy`, `enforce_block_level_slashing_caps`.

Rejections: `OutsideLookback`, `DuplicateEvidence`, `BlockCapExceeded`, `PayloadTooLarge`, `AdmissionPuzzleHashMismatch`.

### 16.2 Appeal REMARK

```
wire = SLASH_APPEAL_REMARK_MAGIC_V1 || serde_json(SlashAppeal)
```

APIs: `encode_slash_appeal_remark_payload_v1`, `slash_appeal_remark_puzzle_reveal_v1`, `slash_appeal_remark_puzzle_hash_v1`, `build_slash_appeal_remark_spend_bundle`, `parse_slash_appeals_from_conditions`, `enforce_slash_appeal_remark_admission`, `enforce_slash_appeal_mempool_policy`, `enforce_block_level_appeal_caps`.

Appeal-specific rejections: `AppealForUnknownSlash`, `AppealWindowExpired`, `AppealForFinalisedSlash`, `AppealVariantMismatch`, `DuplicateAppeal`, `AppealPayloadTooLarge`.

### 16.3 Spend Signature

Both REMARK bundles spend a coin owned by the reporter (evidence) or appellant (appeal). The spend is signed via the principal's BLS key; the `aggregated_signature` field on the bundle carries that signature. `BondEscrow.lock` is called in a separate spend emitted by the consensus layer on admission.

## 17. Error Types

### 17.1 SlashingError

```rust
#[derive(Debug, thiserror::Error)]
pub enum SlashingError {
    #[error("evidence already processed")] AlreadySlashed,
    #[error("offense epoch {offense_epoch} older than lookback (current {current_epoch})")]
    OffenseTooOld { offense_epoch: u64, current_epoch: u64 },
    #[error("validator not registered: {0}")] ValidatorNotRegistered(u32),
    #[error("reporter not registered: {0}")] ReporterNotRegistered(u32),
    #[error("reporter cannot accuse self (index {0})")] ReporterIsAccused(u32),
    #[error("invalid proposer slashing: {0}")] InvalidProposerSlashing(String),
    #[error("invalid attester slashing: {0}")] InvalidAttesterSlashing(String),
    #[error("invalid indexed attestation: {0}")] InvalidIndexedAttestation(String),
    #[error("attestations do not prove a slashable offense")] AttesterSlashingNotSlashable,
    #[error("attester slashing intersecting indices empty")] EmptySlashableIntersection,
    #[error("invalid block evidence: {0}")] InvalidSlashingEvidence(String),
    #[error("BLS signature verification failed")] BlsVerifyFailed,
    #[error("pending slash book at capacity ({0})")] PendingBookFull(usize),
    #[error("bond lock failed: {0}")] BondLockFailed(String),
    #[error("reorg deeper than retention window")] ReorgTooDeep,
}
```

### 17.2 AppealError

```rust
#[derive(Debug, thiserror::Error)]
pub enum AppealError {
    #[error("unknown evidence hash 0x{0}")] UnknownEvidence(String),
    #[error("appeal window expired (submitted {submitted_at}, window {window}, current {current})")]
    WindowExpired { submitted_at: u64, window: u64, current: u64 },
    #[error("slash already reverted")] SlashAlreadyReverted,
    #[error("slash already finalised")] SlashAlreadyFinalised,
    #[error("appeal variant does not match evidence variant")] VariantMismatch,
    #[error("appeal payload too large: {actual} > {limit}")] PayloadTooLarge { actual: usize, limit: usize },
    #[error("appeal is a byte-duplicate of a prior attempt")] DuplicateAppeal,
    #[error("appeal attempts exceeded ({count} >= {limit})")] TooManyAttempts { count: usize, limit: usize },
    #[error("appellant validator not registered: {0}")] AppellantNotRegistered(u32),
    #[error("appellant bond lock failed: {0}")] AppellantBondLockFailed(String),
    #[error("missing oracle: {0}")] MissingOracle(&'static str),
    #[error("malformed witness: {0}")] MalformedWitness(String),
    #[error("verifier error: {0}")] VerifierError(String),
}
```

### 17.3 BondError, ParticipationError, SlashingRemarkError

```rust
#[derive(Debug, thiserror::Error)]
pub enum BondError {
    #[error("insufficient balance to lock bond: have {have}, need {need}")]
    InsufficientBalance { have: u64, need: u64 },
    #[error("bond tag {tag:?} not found")] TagNotFound { tag: BondTag },
    #[error("bond tag {tag:?} already locked")] DoubleLock { tag: BondTag },
}

#[derive(Debug, thiserror::Error)]
pub enum ParticipationError {
    #[error("index {0} out of range")] IndexOutOfRange(u32),
    #[error("attestation slot {slot} outside current epoch {epoch}")]
    SlotOutsideEpoch { slot: u64, epoch: u64 },
    #[error("attesting indices not strictly ascending")] NonAscendingIndices,
    #[error("duplicate attester index {0}")] DuplicateIndex(u32),
}

#[derive(Debug, thiserror::Error)]
pub enum SlashingRemarkError {
    #[error("payload encode failed: {0}")] Encode(String),
    #[error("spend assembly failed: {0}")] Spend(#[from] SpendError),
    #[error("REMARK puzzle_hash mismatch: got 0x{got}, expected 0x{expected}")]
    AdmissionPuzzleHashMismatch { got: String, expected: String },
    #[error("outside lookback: epoch {epoch}, current {current_epoch}")]
    OutsideLookback { epoch: u64, current_epoch: u64 },
    #[error("duplicate evidence or appeal")] DuplicateEvidence,
    #[error("block cap exceeded: {actual} > {limit}")] BlockCapExceeded { actual: usize, limit: usize },
    #[error("payload too large: {actual} > {limit}")] PayloadTooLarge { actual: usize, limit: usize },
    #[error("appeal for unknown slash 0x{0}")] AppealForUnknownSlash(String),
    #[error("appeal window expired for slash 0x{0}")] AppealWindowExpired(String),
    #[error("appeal for finalised slash 0x{0}")] AppealForFinalisedSlash(String),
    #[error("appeal variant does not match evidence variant")] AppealVariantMismatch,
}
```

## 18. Serialization

| Type | `serde` | `bincode` | JSON wire |
|------|---------|-----------|-----------|
| Offense / evidence / attestation types | Yes | Yes | Yes |
| `SlashingEvidence` / payload enum | Yes | Yes | Yes (REMARK) |
| `SlashAppeal` / payload enum | Yes | Yes | Yes (REMARK) |
| `AppealVerdict` / `AppealAdjudicationResult` | Yes | Yes | No |
| `PendingSlash` / `PendingSlashStatus` / `AppealAttempt` | Yes | Yes | No |
| `SlashingResult` / `PerValidatorSlash` / `FinalisationResult` / `FlagDelta` / `EpochBoundaryReport` / `ReorgReport` | Yes | Yes | No |
| `ParticipationFlags` | Yes | Yes | Yes |
| `SlashingProtection` | Yes | No | Yes (disk) |

Conventions: LE integers, `serde_bytes` for BLS fields, frozen `_V1` prefixes, SHA-256 exclusively, `chia_bls` exclusively.

## 19. Public API Summary

### 19.1 Constants

All of §2 re-exported from `lib.rs`.

### 19.2 Evidence

```rust
ProposerSlashing, AttesterSlashing, InvalidBlockProof — constructors + hash + helpers
SlashingEvidence::proposer / ::attester / ::invalid_block
SlashingEvidence::hash / ::slashable_validators
verify_evidence(..) -> Result<VerifiedEvidence, SlashingError>
verify_evidence_for_inclusion(..) -> Result<VerifiedEvidence, SlashingError>
IndexedAttestation::validate_structure / ::verify_signature
AttestationData::signing_root
```

### 19.3 Appeals

```rust
ProposerSlashingAppeal::new / AttesterSlashingAppeal::new / InvalidBlockAppeal::new
SlashAppeal::new / ::hash
verify_appeal(..) -> Result<AppealVerdict, AppealError>
AppealAdjudicator::adjudicate(..)
```

### 19.4 Manager

```rust
SlashingManager::new / ::set_epoch
SlashingManager::submit_evidence(..)
SlashingManager::submit_appeal(..)
SlashingManager::finalise_expired_slashes(..)
SlashingManager::rewind_on_reorg(..)
SlashingManager::is_slashed / ::is_processed / ::pending / ::prune
PendingSlashBook::*
```

### 19.5 Participation + Inactivity

```rust
ParticipationFlags::set / ::has
ParticipationTracker::new / ::record_attestation / ::rotate_epoch / ::previous_epoch_flags / ::rewind_on_reorg
classify_timeliness(..)
base_reward(eff_bal, total) / compute_flag_deltas(..) / proposer_inclusion_reward(base)
InactivityScoreTracker::new / ::update_for_epoch / ::epoch_penalties / ::rewind_on_reorg
in_finality_stall(current, finalized)
```

### 19.6 Orchestration + Genesis

```rust
run_epoch_boundary(..) -> EpochBoundaryReport
rewind_all_on_reorg(..) -> ReorgReport
SlashingSystem::genesis(&GenesisParameters)
```

### 19.7 REMARK Admission

Both `*_evidence_*` and `*_appeal_*` families.

### 19.8 Slashing Protection

As §14.

## 20. Directory Structure

```
dig-slashing/
├── Cargo.toml
├── README.md
├── docs/
│   └── resources/
│       └── SPEC.md                          (this file)
├── src/
│   ├── lib.rs                               Crate root; public re-exports.
│   ├── constants.rs                         §2.
│   ├── error.rs                             §17.
│   ├── evidence/
│   │   ├── mod.rs
│   │   ├── offense.rs                       OffenseType.
│   │   ├── checkpoint.rs                    Checkpoint.
│   │   ├── attestation_data.rs              AttestationData + signing_root.
│   │   ├── indexed_attestation.rs           IndexedAttestation + verify/validate.
│   │   ├── proposer_slashing.rs             SignedBlockHeader + ProposerSlashing.
│   │   ├── attester_slashing.rs             AttesterSlashing + predicates.
│   │   ├── invalid_block.rs                 InvalidBlockProof + InvalidBlockReason.
│   │   ├── envelope.rs                      SlashingEvidence + payload + hash.
│   │   └── verify.rs                        §5 verifiers.
│   ├── appeal/
│   │   ├── mod.rs
│   │   ├── proposer.rs                      ProposerSlashingAppeal + grounds.
│   │   ├── attester.rs                      AttesterSlashingAppeal + grounds.
│   │   ├── invalid_block.rs                 InvalidBlockAppeal + grounds.
│   │   ├── envelope.rs                      SlashAppeal + payload + hash.
│   │   ├── verify.rs                        §6 verify_appeal dispatcher + per-ground.
│   │   └── adjudicator.rs                   AppealAdjudicator.
│   ├── manager.rs                           §7 SlashingManager.
│   ├── pending.rs                           PendingSlashBook + PendingSlash + AppealAttempt.
│   ├── lifecycle.rs                         PendingSlashStatus helpers.
│   ├── result.rs                            SlashingResult, PerValidatorSlash,
│   │                                         AppealAdjudicationResult, FinalisationResult,
│   │                                         EpochBoundaryReport, ReorgReport.
│   ├── participation/
│   │   ├── mod.rs
│   │   ├── flags.rs
│   │   ├── tracker.rs
│   │   ├── timeliness.rs
│   │   └── rewards.rs
│   ├── inactivity/
│   │   ├── mod.rs
│   │   ├── score.rs
│   │   └── penalty.rs
│   ├── orchestration.rs                     §10 run_epoch_boundary, §13 rewind_all_on_reorg.
│   ├── system.rs                            §11 SlashingSystem + GenesisParameters.
│   ├── traits.rs                            §15.
│   ├── remark/
│   │   ├── mod.rs
│   │   ├── evidence_wire.rs
│   │   ├── appeal_wire.rs
│   │   ├── parse.rs
│   │   └── policy.rs
│   ├── protection.rs                        §14 SlashingProtection.
│   └── tests/
│       ├── fixtures.rs
│       ├── mock_validator_set.rs
│       ├── mock_effective_balances.rs
│       ├── mock_bond_escrow.rs
│       ├── mock_reward_payout.rs
│       ├── mock_reward_clawback.rs
│       ├── mock_invalid_block_oracle.rs
│       ├── mock_justification.rs
│       └── mock_proposer.rs
├── tests/
│   │  One file per requirement in §22. Naming: `dsl_NNN_<short_name>_test.rs`
│   │  where NNN is the 3-digit requirement ID. See §22 catalogue for full list.
│   │  Every requirement in §22 is traced to exactly one test file here.
│   ├── dsl_001_offense_type_bps_mapping_test.rs
│   ├── dsl_002_evidence_hash_determinism_test.rs
│   │   ... (see §22) ...
│   └── dsl_130_rewind_all_on_reorg_test.rs
└── benches/
    ├── verify_attester_slashing.rs
    ├── verify_attester_appeal.rs
    └── participation_rewards.rs
```

## 21. Crate Boundary

### 21.1 Owned

| Concern | Crates used |
|---------|-------------|
| OffenseType, evidence + attestation types | `chia-protocol`, `serde_bytes` |
| Evidence hash | `chia-sha2` |
| Per-offense verifiers | `chia-bls::verify`, `chia-bls::aggregate_verify`, `dig-block::block_signing_message` |
| Appeal envelopes + 3 payload variants + grounds ||
| Per-ground appeal verifiers | `chia-bls::verify`, `chia-bls::aggregate_verify` |
| AppealAdjudicator ||
| SlashingManager + PendingSlashBook ||
| Reward/bond routing (via traits) ||
| Epoch-boundary orchestration ||
| Reorg rewind orchestration ||
| Genesis / SlashingSystem ||
| Participation + rewards | `num-integer::Roots::sqrt` |
| Inactivity accounting (continuous) ||
| Timeliness classification ||
| Evidence + appeal REMARK wires, puzzles, admission, policy | `serde_json`, `clvm-utils::tree_hash` |
| SlashingProtection | `serde_json`, `hex`, `tracing` |
| Error types (4) | `thiserror` |
| Trait definitions (ValidatorView, EffectiveBalanceView, PublicKeyLookup, CollateralSlasher, BondEscrow, RewardPayout, RewardClawback, JustificationView, ProposerView, InvalidBlockOracle) | `chia-bls::PublicKey` |

### 21.2 Not Owned

| Concern | Owner |
|---------|-------|
| Block format + signing-message helpers | `dig-block` |
| Validator set, stake math, activation/exit queues | `dig-consensus` |
| Effective-balance calculation | `dig-consensus` |
| Collateral manager | `dig-collateral` |
| Bond escrow storage | `dig-collateral` or dedicated crate |
| Reward account storage | `dig-consensus` or reward-distribution crate |
| Fork choice, justification, finalisation, proposer selection | `dig-consensus` |
| Block re-execution engine | `dig-block` / `dig-clvm` |
| Epoch arithmetic | `dig-epoch` |
| Attestation gossip + aggregation + inclusion | `dig-gossip` / `dig-consensus` |
| Network constants | `dig-constants` |
| Mempool pending set | `dig-mempool` |
| CLVM execution | `dig-clvm` (dev-dep only) |
| DFSP / storage-provider slashing | Separate future crate |
| Sync committee + sync-committee slashings | Not in DIG (weight units reserved but unassigned) |

### 21.3 Dependency Direction

```
dig-slashing
    ├──► dig-block, dig-epoch, dig-constants
    ├──► chia-protocol, chia-bls, chia-sha2, chia-sdk-types, clvm-utils
    ├──► num-integer
    ├──► serde, serde_json, serde_bytes, bincode
    ├──► thiserror, hex, tracing
    └──► parking_lot (optional)

Downstream:
    dig-consensus   ──► dig-slashing (SlashingManager, run_epoch_boundary,
                                      ValidatorView/EffectiveBalanceView/
                                      ProposerView/JustificationView/
                                      RewardPayout/RewardClawback impls)
    dig-collateral  ──► dig-slashing (CollateralSlasher impl, BondEscrow impl)
    dig-mempool     ──► dig-slashing (evidence + appeal REMARK admission + policy)
    validator-app   ──► dig-slashing (SlashingProtection)
    full-node       ──► dig-slashing (verification on block admission,
                                      epoch-boundary orchestration)

    dig-block, dig-epoch, dig-constants ── (no dependency on dig-slashing)
```

## 22. Requirements Catalogue & Test File Mapping

Each requirement has a unique ID `DSL-NNN` and a dedicated test file `tests/dsl_NNN_<short_name>_test.rs`. Every requirement is testable and traced.

### 22.1 Evidence & Attestation Types

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-001 | `OffenseType::base_penalty_bps` returns 500/300/100/100 for the four variants; all `< MAX_PENALTY_BPS`. | `dsl_001_offense_type_bps_mapping_test.rs` |
| DSL-002 | `SlashingEvidence::hash` is deterministic; mutation of any field shifts the hash. | `dsl_002_evidence_hash_determinism_test.rs` |
| DSL-003 | `Checkpoint` serde + equality + hash round-trip. | `dsl_003_checkpoint_roundtrip_test.rs` |
| DSL-004 | `AttestationData::signing_root` is deterministic, domain-prefixed, changes on any field mutation. | `dsl_004_attestation_data_signing_root_test.rs` |
| DSL-005 | `IndexedAttestation::validate_structure` rejects non-ascending, duplicates, empty, over committee cap, bad sig width. | `dsl_005_indexed_attestation_validate_structure_test.rs` |
| DSL-006 | `IndexedAttestation::verify_signature` aggregate BLS verify succeeds on valid aggregate; fails on corruption. | `dsl_006_indexed_attestation_verify_signature_test.rs` |
| DSL-007 | `AttesterSlashing::slashable_indices` returns the sorted intersection. | `dsl_007_attester_slashing_slashable_indices_test.rs` |
| DSL-008 | `InvalidBlockProof` + `InvalidBlockReason` construction + round-trip. | `dsl_008_invalid_block_proof_roundtrip_test.rs` |
| DSL-009 | `SignedBlockHeader` serde round-trip. | `dsl_009_signed_block_header_roundtrip_test.rs` |
| DSL-010 | `SlashingEvidence::slashable_validators` returns 1 for Proposer/InvalidBlock; N for Attester. | `dsl_010_slashable_validators_list_test.rs` |

### 22.2 Evidence Verification

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-011 | `verify_evidence` rejects `OffenseTooOld` when `evidence.epoch + SLASH_LOOKBACK_EPOCHS < current_epoch`. | `dsl_011_verify_evidence_offense_too_old_test.rs` |
| DSL-012 | `verify_evidence` rejects `ReporterIsAccused` when `reporter_index ∈ slashable_validators`. | `dsl_012_verify_evidence_reporter_is_accused_test.rs` |
| DSL-013 | `verify_proposer_slashing` enforces same-slot, same-proposer, different-root, valid sigs, active validator. | `dsl_013_verify_proposer_slashing_preconditions_test.rs` |
| DSL-014 | `verify_attester_slashing` accepts double-vote (same target, different data). | `dsl_014_verify_attester_double_vote_predicate_test.rs` |
| DSL-015 | `verify_attester_slashing` accepts surround-vote (a.src < b.src AND a.tgt > b.tgt). | `dsl_015_verify_attester_surround_vote_predicate_test.rs` |
| DSL-016 | `verify_attester_slashing` rejects `EmptySlashableIntersection`. | `dsl_016_verify_attester_empty_intersection_test.rs` |
| DSL-017 | `verify_attester_slashing` rejects `AttesterSlashingNotSlashable` (neither predicate holds). | `dsl_017_verify_attester_not_slashable_test.rs` |
| DSL-018 | `verify_invalid_block` enforces signature over `block_signing_message`. | `dsl_018_verify_invalid_block_signature_over_domain_test.rs` |
| DSL-019 | `verify_invalid_block` rejects `header.epoch != evidence.epoch`. | `dsl_019_verify_invalid_block_epoch_mismatch_test.rs` |
| DSL-020 | `verify_invalid_block` calls `InvalidBlockOracle::verify_failure` when oracle supplied. | `dsl_020_verify_invalid_block_oracle_called_test.rs` |
| DSL-021 | `verify_evidence_for_inclusion` behaves identically to `verify_evidence` minus state mutation. | `dsl_021_verify_evidence_for_inclusion_parity_test.rs` |

### 22.3 Optimistic Slashing Lifecycle

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-022 | `submit_evidence` applies `base_slash = max(eff_bal*bps/10_000, eff_bal/32)`. | `dsl_022_submit_evidence_base_slash_formula_test.rs` |
| DSL-023 | `submit_evidence` escrows `REPORTER_BOND_MOJOS` via `BondEscrow::lock`. | `dsl_023_submit_evidence_escrows_reporter_bond_test.rs` |
| DSL-024 | `submit_evidence` creates `PendingSlash { status: Accepted, window_expires_at_epoch }`. | `dsl_024_submit_evidence_creates_pending_accepted_test.rs` |
| DSL-025 | `submit_evidence` routes `wb_reward` to reporter puzzle hash + `prop_reward` to block proposer puzzle hash. | `dsl_025_submit_evidence_reward_routing_test.rs` |
| DSL-026 | `submit_evidence` rejects `AlreadySlashed` on duplicate. | `dsl_026_submit_evidence_already_slashed_test.rs` |
| DSL-027 | `submit_evidence` rejects `PendingBookFull` at capacity. | `dsl_027_submit_evidence_book_full_test.rs` |
| DSL-028 | `submit_evidence` rejects `BondLockFailed` on insufficient reporter stake. | `dsl_028_submit_evidence_bond_lock_failed_test.rs` |
| DSL-029 | `finalise_expired_slashes` transitions Accepted / ChallengeOpen to Finalised after window. | `dsl_029_finalise_transitions_to_finalised_test.rs` |
| DSL-030 | `finalise_expired_slashes` applies correlation penalty `eff_bal * min(cohort*3, total) / total`. | `dsl_030_finalise_applies_correlation_penalty_test.rs` |
| DSL-031 | `finalise_expired_slashes` returns reporter bond in full. | `dsl_031_finalise_returns_reporter_bond_test.rs` |
| DSL-032 | `finalise_expired_slashes` schedules exit lock until `current + SLASH_LOCK_EPOCHS`. | `dsl_032_finalise_schedules_exit_lock_test.rs` |
| DSL-033 | `finalise_expired_slashes` skips already-Reverted slashes. | `dsl_033_finalise_skips_reverted_test.rs` |

### 22.4 Appeal Verification — Proposer

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-034 | `ProposerAppeal::HeadersIdentical` sustained when headers byte-equal. | `dsl_034_proposer_appeal_headers_identical_sustained_test.rs` |
| DSL-035 | `ProposerAppeal::ProposerIndexMismatch` sustained. | `dsl_035_proposer_appeal_proposer_index_mismatch_test.rs` |
| DSL-036 | `ProposerAppeal::SignatureAInvalid` sustained when sig_a fails BLS verify. | `dsl_036_proposer_appeal_signature_a_invalid_test.rs` |
| DSL-037 | `ProposerAppeal::SignatureBInvalid` sustained. | `dsl_037_proposer_appeal_signature_b_invalid_test.rs` |
| DSL-038 | `ProposerAppeal::SlotMismatch` sustained. | `dsl_038_proposer_appeal_slot_mismatch_test.rs` |
| DSL-039 | `ProposerAppeal::ValidatorNotActiveAtEpoch` sustained. | `dsl_039_proposer_appeal_validator_not_active_test.rs` |
| DSL-040 | `ProposerAppeal` rejected when claim is false. | `dsl_040_proposer_appeal_rejected_on_false_claim_test.rs` |

### 22.5 Appeal Verification — Attester

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-041 | `AttesterAppeal::AttestationsIdentical` sustained. | `dsl_041_attester_appeal_attestations_identical_test.rs` |
| DSL-042 | `AttesterAppeal::NotSlashableByPredicate` sustained. | `dsl_042_attester_appeal_not_slashable_predicate_test.rs` |
| DSL-043 | `AttesterAppeal::EmptyIntersection` sustained. | `dsl_043_attester_appeal_empty_intersection_test.rs` |
| DSL-044 | `AttesterAppeal::SignatureAInvalid` sustained. | `dsl_044_attester_appeal_signature_a_invalid_test.rs` |
| DSL-045 | `AttesterAppeal::SignatureBInvalid` sustained. | `dsl_045_attester_appeal_signature_b_invalid_test.rs` |
| DSL-046 | `AttesterAppeal::InvalidIndexedAttestationStructure` sustained. | `dsl_046_attester_appeal_invalid_structure_test.rs` |
| DSL-047 | `AttesterAppeal::ValidatorNotInIntersection` sustained for named index. | `dsl_047_attester_appeal_validator_not_in_intersection_test.rs` |
| DSL-048 | `AttesterAppeal` rejected on genuine slash. | `dsl_048_attester_appeal_rejected_genuine_test.rs` |

### 22.6 Appeal Verification — Invalid Block

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-049 | `InvalidBlockAppeal::BlockActuallyValid` sustained when oracle re-executes `Valid`. | `dsl_049_invalid_block_appeal_block_valid_test.rs` |
| DSL-050 | `InvalidBlockAppeal::ProposerSignatureInvalid` sustained. | `dsl_050_invalid_block_appeal_sig_invalid_test.rs` |
| DSL-051 | `InvalidBlockAppeal::FailureReasonMismatch` sustained when oracle reports different reason. | `dsl_051_invalid_block_appeal_reason_mismatch_test.rs` |
| DSL-052 | `InvalidBlockAppeal::EvidenceEpochMismatch` sustained. | `dsl_052_invalid_block_appeal_epoch_mismatch_test.rs` |
| DSL-053 | `InvalidBlockAppeal` returns `MissingOracle` when re-execution needed but no oracle. | `dsl_053_invalid_block_appeal_missing_oracle_test.rs` |
| DSL-054 | `InvalidBlockAppeal` rejected on genuine invalid block. | `dsl_054_invalid_block_appeal_rejected_test.rs` |

### 22.7 Appeal Submission Preconditions

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-055 | `submit_appeal` rejects `UnknownEvidence` for non-existent hash. | `dsl_055_submit_appeal_unknown_evidence_test.rs` |
| DSL-056 | `submit_appeal` rejects `WindowExpired` when filed after window. | `dsl_056_submit_appeal_window_expired_test.rs` |
| DSL-057 | `submit_appeal` rejects `VariantMismatch` on mismatched payload types. | `dsl_057_submit_appeal_variant_mismatch_test.rs` |
| DSL-058 | `submit_appeal` rejects `DuplicateAppeal` on byte-equal prior attempt. | `dsl_058_submit_appeal_duplicate_test.rs` |
| DSL-059 | `submit_appeal` rejects `TooManyAttempts` at `MAX_APPEAL_ATTEMPTS_PER_SLASH`. | `dsl_059_submit_appeal_too_many_attempts_test.rs` |
| DSL-060 | `submit_appeal` rejects `SlashAlreadyReverted`. | `dsl_060_submit_appeal_already_reverted_test.rs` |
| DSL-061 | `submit_appeal` rejects `SlashAlreadyFinalised`. | `dsl_061_submit_appeal_already_finalised_test.rs` |
| DSL-062 | `submit_appeal` escrows `APPELLANT_BOND_MOJOS`. | `dsl_062_submit_appeal_escrows_appellant_bond_test.rs` |
| DSL-063 | `submit_appeal` rejects `PayloadTooLarge` > `MAX_APPEAL_PAYLOAD_BYTES`. | `dsl_063_submit_appeal_payload_too_large_test.rs` |

### 22.8 Adjudicator

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-064 | Sustained reverts per-validator base slash via `credit_stake`. | `dsl_064_adjudicate_sustained_reverts_base_slash_test.rs` |
| DSL-065 | Sustained reverts collateral via `CollateralSlasher::credit`. | `dsl_065_adjudicate_sustained_reverts_collateral_test.rs` |
| DSL-066 | Sustained calls `restore_status` on every slashed validator. | `dsl_066_adjudicate_sustained_restores_status_test.rs` |
| DSL-067 | Sustained claws back `wb_reward` + `prop_reward`. | `dsl_067_adjudicate_sustained_clawback_rewards_test.rs` |
| DSL-068 | Sustained forfeits reporter bond; appellant_award = forfeited × 50%; burn = rest. | `dsl_068_adjudicate_sustained_bond_split_test.rs` |
| DSL-069 | Sustained slashes reporter via `InvalidBlock` base formula. | `dsl_069_adjudicate_sustained_reporter_penalty_test.rs` |
| DSL-070 | Sustained transitions pending to `Reverted`. | `dsl_070_adjudicate_sustained_status_reverted_test.rs` |
| DSL-071 | Rejected forfeits appellant bond; reporter_award = 50%; burn = rest. | `dsl_071_adjudicate_rejected_bond_split_test.rs` |
| DSL-072 | Rejected leaves pending in `ChallengeOpen { appeal_count: n+1 }`. | `dsl_072_adjudicate_rejected_challenge_open_test.rs` |
| DSL-073 | Clawback shortfall absorbed from forfeited bond; residue burned. | `dsl_073_adjudicate_clawback_shortfall_test.rs` |

### 22.9 Participation & Rewards

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-074 | `ParticipationFlags::set` + `has` per bit. | `dsl_074_participation_flags_bits_test.rs` |
| DSL-075 | `classify_timeliness` sets `TIMELY_SOURCE` iff delay ∈ [1,5] AND source_justified. | `dsl_075_classify_timely_source_test.rs` |
| DSL-076 | `classify_timeliness` sets `TIMELY_TARGET` iff delay ∈ [1,32] AND target canonical. | `dsl_076_classify_timely_target_test.rs` |
| DSL-077 | `classify_timeliness` sets `TIMELY_HEAD` iff delay == 1 AND head canonical. | `dsl_077_classify_timely_head_test.rs` |
| DSL-078 | `ParticipationTracker::record_attestation` sets flags for each ascending index. | `dsl_078_participation_tracker_record_test.rs` |
| DSL-079 | `ParticipationTracker::record_attestation` rejects non-ascending indices. | `dsl_079_participation_tracker_non_ascending_test.rs` |
| DSL-080 | `ParticipationTracker::rotate_epoch` swaps prev/current, zeroes current. | `dsl_080_participation_tracker_rotate_test.rs` |
| DSL-081 | `base_reward = eff_bal * BASE_REWARD_FACTOR / isqrt(total_active)`. | `dsl_081_base_reward_formula_test.rs` |
| DSL-082 | `compute_flag_deltas` awards on hit. | `dsl_082_flag_deltas_reward_on_hit_test.rs` |
| DSL-083 | `compute_flag_deltas` penalises SOURCE + TARGET miss; does not penalise HEAD miss. | `dsl_083_flag_deltas_penalty_head_exempt_test.rs` |
| DSL-084 | `compute_flag_deltas` zeroes rewards in finality stall; penalties still apply. | `dsl_084_flag_deltas_stall_zero_rewards_test.rs` |
| DSL-085 | `proposer_inclusion_reward = base * 8 / (64 - 8)`. | `dsl_085_proposer_inclusion_reward_formula_test.rs` |
| DSL-086 | `WEIGHT_DENOMINATOR == 64` with 2 units unassigned (no sync committee). | `dsl_086_weight_denominator_no_sync_test.rs` |

### 22.10 Inactivity

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-087 | `in_finality_stall` true iff `current - finalized > 4`. | `dsl_087_in_finality_stall_threshold_test.rs` |
| DSL-088 | `InactivityScoreTracker::update` decrements on target hit. | `dsl_088_inactivity_score_hit_decrement_test.rs` |
| DSL-089 | `InactivityScoreTracker::update` increments by 4 on target miss + stall. | `dsl_089_inactivity_score_miss_in_stall_increment_test.rs` |
| DSL-090 | `InactivityScoreTracker::update` recovers by 16 per epoch out of stall. | `dsl_090_inactivity_score_out_of_stall_recovery_test.rs` |
| DSL-091 | `InactivityScoreTracker::epoch_penalties` returns empty vec out of stall. | `dsl_091_inactivity_penalty_no_stall_empty_test.rs` |
| DSL-092 | `InactivityScoreTracker::epoch_penalties` formula `eff_bal * score / 16_777_216`. | `dsl_092_inactivity_penalty_formula_test.rs` |
| DSL-093 | `InactivityScoreTracker::resize_for` grows scores vec; new entries start at 0. | `dsl_093_inactivity_resize_test.rs` |

### 22.11 Slashing Protection

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-094 | `check_proposal_slot` monotonic; fails at equal or lower slot after record. | `dsl_094_protection_proposal_monotonic_test.rs` |
| DSL-095 | `check_attestation` fails on same (source,target) with different hash. | `dsl_095_protection_attestation_same_epoch_different_hash_test.rs` |
| DSL-096 | `check_attestation` rejects surround-vote via `would_surround` self-check. | `dsl_096_protection_surround_vote_self_check_test.rs` |
| DSL-097 | `record_proposal` + `record_attestation` persist watermarks. | `dsl_097_protection_record_persist_test.rs` |
| DSL-098 | `rewind_attestation_to_epoch` clears block hash. | `dsl_098_protection_rewind_attestation_clears_hash_test.rs` |
| DSL-099 | `reconcile_with_chain_tip` rewinds both watermarks. | `dsl_099_protection_reconcile_with_tip_test.rs` |
| DSL-100 | Legacy JSON (no hash field) loads with `None`. | `dsl_100_protection_legacy_json_test.rs` |
| DSL-101 | Save/load round-trip preserves all fields. | `dsl_101_protection_save_load_roundtrip_test.rs` |

### 22.12 REMARK Admission — Evidence

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-102 | Evidence REMARK wire: magic prefix + JSON round-trip. | `dsl_102_evidence_remark_wire_roundtrip_test.rs` |
| DSL-103 | Evidence puzzle reveal emits exactly one REMARK; parseable. | `dsl_103_evidence_puzzle_reveal_emits_one_remark_test.rs` |
| DSL-104 | Admission accepts bundle with matching `puzzle_hash`. | `dsl_104_evidence_admission_matching_coin_test.rs` |
| DSL-105 | Admission rejects mismatched payload. | `dsl_105_evidence_admission_mismatch_rejected_test.rs` |
| DSL-106 | Mempool policy rejects expired evidence. | `dsl_106_evidence_mempool_expired_rejected_test.rs` |
| DSL-107 | Mempool policy rejects duplicate evidence. | `dsl_107_evidence_mempool_duplicate_rejected_test.rs` |
| DSL-108 | Block cap: `> MAX_SLASH_PROPOSALS_PER_BLOCK` rejected. | `dsl_108_evidence_block_cap_test.rs` |
| DSL-109 | Payload cap: `> MAX_SLASH_PROPOSAL_PAYLOAD_BYTES` rejected. | `dsl_109_evidence_payload_cap_test.rs` |

### 22.13 REMARK Admission — Appeal

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-110 | Appeal REMARK wire round-trip. | `dsl_110_appeal_remark_wire_roundtrip_test.rs` |
| DSL-111 | Appeal puzzle reveal emits one REMARK; parseable. | `dsl_111_appeal_puzzle_reveal_emits_one_remark_test.rs` |
| DSL-112 | Appeal admission matching coin. | `dsl_112_appeal_admission_matching_coin_test.rs` |
| DSL-113 | Appeal admission mismatched rejected. | `dsl_113_appeal_admission_mismatch_rejected_test.rs` |
| DSL-114 | Appeal mempool `AppealForUnknownSlash`. | `dsl_114_appeal_mempool_unknown_slash_test.rs` |
| DSL-115 | Appeal mempool `AppealWindowExpired`. | `dsl_115_appeal_mempool_window_expired_test.rs` |
| DSL-116 | Appeal mempool `AppealForFinalisedSlash`. | `dsl_116_appeal_mempool_finalised_slash_test.rs` |
| DSL-117 | Appeal mempool `AppealVariantMismatch`. | `dsl_117_appeal_mempool_variant_mismatch_test.rs` |
| DSL-118 | Appeal mempool duplicate rejected. | `dsl_118_appeal_mempool_duplicate_test.rs` |
| DSL-119 | Appeal block cap: `> MAX_APPEALS_PER_BLOCK` rejected. | `dsl_119_appeal_block_cap_test.rs` |
| DSL-120 | Appeal payload cap: `> MAX_APPEAL_PAYLOAD_BYTES` rejected. | `dsl_120_appeal_payload_cap_test.rs` |

### 22.14 Bonds, Rewards, Routing

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-121 | `BondEscrow::lock` returns `InsufficientBalance` when principal stake < amount. | `dsl_121_bond_lock_insufficient_balance_test.rs` |
| DSL-122 | `BondEscrow::forfeit` returns forfeited mojos; zeroes the tag. | `dsl_122_bond_forfeit_returns_mojos_test.rs` |
| DSL-123 | `BondEscrow::release` returns full escrowed amount on finalisation. | `dsl_123_bond_release_full_on_finalise_test.rs` |
| DSL-124 | `REPORTER_BOND_MOJOS == MIN_EFFECTIVE_BALANCE / 64`. | `dsl_124_bond_reporter_size_test.rs` |
| DSL-125 | `APPELLANT_BOND_MOJOS == MIN_EFFECTIVE_BALANCE / 64`. | `dsl_125_bond_appellant_size_test.rs` |
| DSL-126 | `BOND_AWARD_TO_WINNER_BPS = 5_000` (50/50 winner/burn split). | `dsl_126_bond_award_50_50_split_test.rs` |

### 22.15 Orchestration, Genesis, Reorg

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-127 | `run_epoch_boundary` runs (rotate, deltas, inactivity update, inactivity penalties, finalise, rotate tracker, advance epoch, resize, prune) in fixed order. | `dsl_127_epoch_boundary_order_test.rs` |
| DSL-128 | `SlashingSystem::genesis` initialises empty state at `genesis_epoch`. | `dsl_128_genesis_initialisation_test.rs` |
| DSL-129 | `SlashingManager::rewind_on_reorg` drops pending slashes submitted at `epoch > new_tip_epoch`; credits base slash back; releases reporter bond. | `dsl_129_manager_rewind_on_reorg_test.rs` |
| DSL-130 | `rewind_all_on_reorg` orchestrates manager + participation + inactivity + protection rewinds; returns `ReorgReport`. `ReorgTooDeep` when depth > `CORRELATION_WINDOW_EPOCHS`. | `dsl_130_rewind_all_on_reorg_test.rs` |

### 22.17 External-State Traits

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-131 | `ValidatorEntry::slash_absolute(amount, epoch)` saturates at current stake; returns mojos actually debited. | `dsl_131_validator_entry_slash_absolute_saturation_test.rs` |
| DSL-132 | `ValidatorEntry::credit_stake(amount)` adds to stake; inverse of `slash_absolute`; saturates at u64::MAX. | `dsl_132_validator_entry_credit_stake_test.rs` |
| DSL-133 | `ValidatorEntry::restore_status()` clears Slashed → Active; returns `true` iff status changed; idempotent. | `dsl_133_validator_entry_restore_status_test.rs` |
| DSL-134 | `ValidatorEntry::is_active_at_epoch(epoch)` returns `true` iff `activation_epoch <= epoch < exit_epoch`. | `dsl_134_validator_entry_is_active_boundary_test.rs` |
| DSL-135 | `ValidatorEntry::schedule_exit(exit_lock_until_epoch)` persists exit-lock epoch. | `dsl_135_validator_entry_schedule_exit_test.rs` |
| DSL-136 | `ValidatorView::get(idx) / get_mut(idx)` return Some for live idx, None out-of-range. | `dsl_136_validator_view_get_contract_test.rs` |
| DSL-137 | `EffectiveBalanceView::get(idx)` + `total_active()` return per-validator and sum-of-active balances. | `dsl_137_effective_balance_view_test.rs` |
| DSL-138 | `PublicKeyLookup::pubkey_of(idx)` returns validator pubkey; blanket impl for ValidatorView. | `dsl_138_public_key_lookup_test.rs` |
| DSL-139 | `CollateralSlasher::slash` + `credit` symmetric; `NoCollateral` is soft failure. | `dsl_139_collateral_slasher_symmetry_test.rs` |
| DSL-140 | `BondEscrow::escrowed(principal_idx, tag)` returns current amount; 0 for unknown tag; no panic. | `dsl_140_bond_escrowed_query_test.rs` |
| DSL-141 | `RewardPayout::pay(principal_ph, amount)` credits reward account; accumulates on repeat calls. | `dsl_141_reward_payout_pay_test.rs` |
| DSL-142 | `RewardClawback::claw_back` returns actual mojos deducted (0..=amount); partial return permitted. | `dsl_142_reward_clawback_partial_test.rs` |
| DSL-143 | `JustificationView` exposes checkpoints + canonical root queries; read-only. | `dsl_143_justification_view_contract_test.rs` |
| DSL-144 | `ProposerView::proposer_at_slot(slot)` returns Some for committed; None for future/missed. | `dsl_144_proposer_view_test.rs` |
| DSL-145 | `InvalidBlockOracle::re_execute` deterministic: same inputs → same `ExecutionOutcome`. | `dsl_145_invalid_block_oracle_determinism_test.rs` |

### 22.18 Gap-Fill Requirements

Added after the v0.4 audit to close contract gaps identified in §7.1 (PendingSlashBook ops), §7.2 (Manager queries), §4 (correlation saturation), §5.1 (short-circuit), §8.2/§9.2/§14.3 (reorg helpers), and §18 (ParticipationFlags serde).

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-146 | `PendingSlashBook` basic ops: `new/insert/get/get_mut/remove/len` map-like contract. | `dsl_146_pending_slash_book_basic_ops_test.rs` |
| DSL-147 | `PendingSlashBook::expired_by(current_epoch)` returns hashes with `window_expires_at_epoch < current_epoch` AND status in {Accepted, ChallengeOpen}. | `dsl_147_pending_slash_book_expired_by_test.rs` |
| DSL-148 | `SlashingManager::new(epoch)` initialises empty state; `set_epoch(e)` updates current_epoch. | `dsl_148_slashing_manager_new_set_epoch_test.rs` |
| DSL-149 | `SlashingManager::is_slashed(idx, validator_set)` delegates to `ValidatorView::get(idx)?.is_slashed()`. | `dsl_149_slashing_manager_is_slashed_test.rs` |
| DSL-150 | `SlashingManager::is_processed`, `pending`, `prune(before_epoch)` — query + maintenance contracts. | `dsl_150_slashing_manager_is_processed_pending_prune_test.rs` |
| DSL-151 | Correlation penalty clamps `min(cohort_sum * 3, total_active_balance)`; saturating multiplication; `total_active=0` guarded. | `dsl_151_correlation_penalty_saturation_clamp_test.rs` |
| DSL-152 | `submit_evidence` propagates `ReporterIsAccused` BEFORE any bond lock, reward, or state mutation. | `dsl_152_submit_evidence_reporter_is_accused_short_circuit_test.rs` |
| DSL-153 | `ParticipationTracker::rewind_on_reorg(depth, validator_count)` restores ring-buffer snapshot. | `dsl_153_participation_tracker_rewind_on_reorg_test.rs` |
| DSL-154 | `ParticipationFlags(u8)` serde roundtrip byte-exact via bincode + serde_json. | `dsl_154_participation_flags_serde_roundtrip_test.rs` |
| DSL-155 | `InactivityScoreTracker::rewind_on_reorg(depth)` restores ring-buffer snapshot; depth bounded by `CORRELATION_WINDOW_EPOCHS`. | `dsl_155_inactivity_tracker_rewind_on_reorg_test.rs` |
| DSL-156 | `SlashingProtection::rewind_proposal_to_slot(new_tip_slot)` lowers slot when higher; idempotent. | `dsl_156_protection_rewind_proposal_to_slot_test.rs` |

### 22.19 Gap Fills 2 — Serde + Defensive + Variants

Added after the second-pass audit to close serde-contract gaps, defensive-skip semantics for `submit_evidence`, `SlashAppeal` hash contract, and `BondTag` variant distinguishability.

| ID | Requirement | Test File |
|----|-------------|-----------|
| DSL-157 | `SlashingEvidence` + `SlashingEvidencePayload` round-trip via bincode + serde_json for all 3 payload variants; serde_bytes encoding preserved. | `dsl_157_slashing_evidence_serde_roundtrip_test.rs` |
| DSL-158 | `IndexedAttestation` round-trips via bincode + serde_json preserving index order, signature serde_bytes, nested AttestationData. | `dsl_158_indexed_attestation_serde_roundtrip_test.rs` |
| DSL-159 | `SlashAppeal::hash()` deterministic + sensitive to each field mutation; domain-prefixed under `DOMAIN_SLASH_APPEAL`. | `dsl_159_slash_appeal_hash_determinism_test.rs` |
| DSL-160 | `SlashAppeal` + `SlashAppealPayload` + all ground enums round-trip via bincode + serde_json. | `dsl_160_slash_appeal_serde_roundtrip_test.rs` |
| DSL-161 | `PendingSlash` + `PendingSlashStatus` (4 variants) + `AppealAttempt` + `AppealOutcome` (3 variants) round-trip via bincode. | `dsl_161_pending_slash_serde_roundtrip_test.rs` |
| DSL-162 | `submit_evidence` per-validator loop uniformly skips indices flagged `is_slashed()`; no slash, no collateral debit, no window record, no result entry; evidence still marked processed. | `dsl_162_submit_evidence_skips_already_slashed_test.rs` |
| DSL-163 | `SlashingResult` + `PerValidatorSlash` + `FinalisationResult` round-trip via bincode + serde_json. | `dsl_163_slashing_result_serde_roundtrip_test.rs` |
| DSL-164 | `AppealAdjudicationResult` round-trips via bincode + serde_json; sustained and rejected cases preserved. | `dsl_164_appeal_adjudication_result_serde_test.rs` |
| DSL-165 | `EpochBoundaryReport` + `ReorgReport` + `FlagDelta` round-trip via bincode + serde_json. | `dsl_165_epoch_boundary_reorg_report_serde_test.rs` |
| DSL-166 | `BondTag::Reporter(h)` vs `BondTag::Appellant(h)` distinguishable via PartialEq + Hash + separate escrow slots + serde discriminator; Copy derive works. | `dsl_166_bond_tag_variants_distinguishable_test.rs` |

### 22.16 Traceability Rules

1. Every requirement must have exactly one `dsl_NNN_*_test.rs` file.
2. Every test file begins with a doc comment `//! Requirement DSL-NNN: <text>` matching §22 exactly.
3. CI verifies 1:1 correspondence. A requirement without a file OR a file without a §22 entry fails the build.
4. Requirements are append-only: new behavior adds DSL-131, DSL-132, ... Existing IDs never change or reorder; a superseded requirement is marked `(superseded by DSL-NNN)` and its file is kept as a regression test.

## 23. Open Items

1. **Governance-level appeal escalation.** Deterministic fraud proofs cover evidence-verifier bugs. Policy questions may warrant a 2nd-tier governance escalation; a `GovernanceAppeal` hook will be added if that path lands.
2. **Succinct validity proof (STARK) path** for `InvalidBlockAppeal::BlockActuallyValid` — lets light clients adjudicate without full re-execution. Reserved.
3. **Appeal-prioritised inclusion.** Block proposers may delay including appeals to run out the window. An `APPEAL_INCLUSION_REWARD` bonus is under consideration.
4. **Appeal bond scaling with accused's effective balance.** Currently flat `MIN_EFFECTIVE_BALANCE / 64`; may scale to prevent grief-reporting of small validators.
5. **Multi-tier appeal windows.** Current: flat 8 epochs. ProposerEquivocation is trivially verifiable; InvalidBlock re-execution is expensive. Tier-specific windows may help.
6. **Threadsafe manager.** `parking_lot::RwLock` behind `threadsafe` cargo feature.
7. **Reporter-proposer collusion rebate.** If the reporter and the block proposer are the same validator (allowed), `wb_reward + prop_reward` both accrue to them. Current behavior matches Ethereum. A future policy may require them to differ.