Skip to main content

dig_epoch/
arithmetic.rs

1//! # `arithmetic` — pure height-to-epoch mapping functions
2//!
3//! **Introduced by:** `STR-002` — Module hierarchy (SPEC §13).
4//!
5//! **Owners:** HEA-001 through HEA-004 (Phase 4).
6//!
7//! **Spec reference:** [`SPEC.md` §5](../../docs/resources/SPEC.md)
8
9/// Sentinel marker proving the module exists and is reachable at
10/// `dig_epoch::arithmetic::STR_002_MODULE_PRESENT`.
11#[doc(hidden)]
12pub const STR_002_MODULE_PRESENT: () = ();
13
14use crate::constants::{BLOCKS_PER_EPOCH, EPOCH_L1_BLOCKS, GENESIS_HEIGHT};
15use crate::error::EpochError;
16
17// -----------------------------------------------------------------------------
18// HEA-001 — epoch_for_block_height
19// -----------------------------------------------------------------------------
20
21/// Maps an L2 block height to its epoch number.
22///
23/// Formula: `(height - 1) / BLOCKS_PER_EPOCH`
24///
25/// Height 1 is genesis (epoch 0). Requires `height >= 1`.
26pub fn epoch_for_block_height(height: u64) -> u64 {
27    (height - 1) / BLOCKS_PER_EPOCH
28}
29
30// -----------------------------------------------------------------------------
31// HEA-002 — first_height_in_epoch / epoch_checkpoint_height
32// -----------------------------------------------------------------------------
33
34/// Returns the first L2 block height in the given epoch.
35///
36/// Formula: `epoch * BLOCKS_PER_EPOCH + 1`
37pub fn first_height_in_epoch(epoch: u64) -> u64 {
38    epoch * BLOCKS_PER_EPOCH + 1
39}
40
41/// Returns the checkpoint (last) L2 block height in the given epoch.
42///
43/// Formula: `(epoch + 1) * BLOCKS_PER_EPOCH`
44pub fn epoch_checkpoint_height(epoch: u64) -> u64 {
45    (epoch + 1) * BLOCKS_PER_EPOCH
46}
47
48// -----------------------------------------------------------------------------
49// HEA-003 — Checkpoint block detection
50// -----------------------------------------------------------------------------
51
52/// Returns true if `height` is the genesis checkpoint block (height == GENESIS_HEIGHT).
53pub fn is_genesis_checkpoint_block(height: u64) -> bool {
54    height == GENESIS_HEIGHT
55}
56
57/// Returns true if `height` is an epoch checkpoint block (divisible by BLOCKS_PER_EPOCH).
58pub fn is_epoch_checkpoint_block(height: u64) -> bool {
59    height % BLOCKS_PER_EPOCH == 0
60}
61
62/// Returns true if `height` is genesis or an epoch checkpoint block.
63pub fn is_checkpoint_class_block(height: u64) -> bool {
64    is_genesis_checkpoint_block(height) || is_epoch_checkpoint_block(height)
65}
66
67/// Enforces the empty-checkpoint-block invariant.
68///
69/// Returns `Ok(())` for non-checkpoint heights regardless of parameters.
70/// Returns `Err(EpochError::CheckpointBlockNotEmpty)` if any count is non-zero
71/// at a checkpoint-class height.
72pub fn ensure_checkpoint_block_empty(
73    height: u64,
74    spend_bundle_count: u32,
75    total_cost: u64,
76    total_fees: u64,
77) -> Result<(), EpochError> {
78    if is_checkpoint_class_block(height)
79        && (spend_bundle_count != 0 || total_cost != 0 || total_fees != 0)
80    {
81        return Err(EpochError::CheckpointBlockNotEmpty(
82            height,
83            spend_bundle_count,
84            total_cost,
85            total_fees,
86        ));
87    }
88    Ok(())
89}
90
91// -----------------------------------------------------------------------------
92// HEA-004 — l1_range_for_epoch
93// -----------------------------------------------------------------------------
94
95/// Returns the `(start_l1_height, end_l1_height)` for a given epoch.
96///
97/// The range is inclusive. Each epoch's L1 window is `EPOCH_L1_BLOCKS` wide.
98pub fn l1_range_for_epoch(genesis_l1_height: u32, epoch: u64) -> (u32, u32) {
99    let start = genesis_l1_height + (epoch as u32 * EPOCH_L1_BLOCKS);
100    let end = start + EPOCH_L1_BLOCKS - 1;
101    (start, end)
102}
103
104// -----------------------------------------------------------------------------
105// HEA-006 — last_committed_height_in_epoch
106// -----------------------------------------------------------------------------
107
108/// Returns the last L2 height included in this epoch's checkpoint.
109///
110/// Caps at `epoch_checkpoint_height(epoch)` even if `tip_height` is higher.
111pub fn last_committed_height_in_epoch(epoch: u64, tip_height: u64) -> u64 {
112    std::cmp::min(tip_height, epoch_checkpoint_height(epoch))
113}
114
115// -----------------------------------------------------------------------------
116// HEA-007 — is_first_block_after_epoch_checkpoint
117// -----------------------------------------------------------------------------
118
119/// Returns true if `height` is the first block after an epoch checkpoint.
120///
121/// True at h=33, 65, 97, … — each epoch's opening block after epoch 0.
122pub fn is_first_block_after_epoch_checkpoint(height: u64) -> bool {
123    height > 1 && (height - 1) % BLOCKS_PER_EPOCH == 0
124}