Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ jobs:
- name: Run unit tests
run: cargo test --workspace --all-targets

# AGENTS.md prescribes verifying with `--all-features`, which enables
# feature-gated code (notably the `strict` epoch-coherence assertions in
# dolos-cardano) that the default-features run above never exercises.
# Keep it green here so the prescribed verification and CI cannot drift
# apart. The three service crates are excluded because their test
# fixtures import synthetic chains that jump from genesis straight to
# epoch 2, tripping the strict assertions inside the fixture itself;
# they keep full coverage in the default-features run above. Remove the
# exclusions once the fixtures build epoch-coherent domains (see
# AGENTS.md, "Code Verification Requirements"). Feature-gated code is
# platform-independent, so one OS keeps the cost bounded.
- name: Run unit tests (all features)
if: runner.os == 'Linux'
run: cargo test --workspace --all-targets --all-features --exclude dolos-minibf --exclude dolos-minikupo --exclude dolos-trp

- name: Run e2e smoke tests
if: runner.os != 'Windows'
run: cargo test --test smoke -- --ignored
Expand Down
15 changes: 14 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,22 @@ All agents working on this repository must verify their modifications by running

3. **Testing**: Run tests to verify functionality
```bash
cargo test --workspace --all-features
cargo test --workspace --all-targets
cargo test --workspace --all-features --exclude dolos-minibf --exclude dolos-minikupo --exclude dolos-trp
Comment thread
scarmuega marked this conversation as resolved.
```

The first command is what CI runs on every platform. The second adds the
feature-gated code the default run never exercises — most importantly the
`strict` feature, dolos-cardano's epoch-coherence assertions. The three
service crates are excluded from the all-features run because their test
fixtures import synthetic chains that jump from a fresh genesis domain
straight to epoch 2, which trips those assertions inside the fixture itself
(the `EpochState` entity is still at epoch 0 when an epoch-2 block rolls);
they keep full coverage under the first command. Remove the exclusions once
the fixtures build epoch-coherent domains. CI (`.github/workflows/ci.yml`)
runs both commands, so a verification that passes locally cannot drift from
what the repository keeps green.

### Code Quality Standards

- All warnings from `cargo clippy` must be resolved before committing changes
Expand Down
41 changes: 29 additions & 12 deletions crates/cardano/src/model/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,19 +174,23 @@ pub(crate) mod testing {
credential in root::any_stake_credential(),
registered_at in prop::option::of(root::any_slot()),
deregistered_at in prop::option::of(root::any_slot()),
epoch in root::any_epoch(),
stake in any_epoch_value(any_stake().boxed()),
pool in any_epoch_value(any_pool_delegation().boxed()),
drep in any_epoch_value(any_drep_delegation().boxed()),
vote_delegated_at in prop::option::of((root::any_slot(), root::any_tx_order())),
retired_pool in prop::option::of(root::any_pool_hash()),
) -> AccountState {
// ESTART rotates stake/pool/drep in lockstep, so a healthy account
// has all three at the same epoch. The `strict` feature asserts
// this; rebase the independently drawn values onto one epoch.
AccountState {
credential,
registered_at,
deregistered_at,
stake,
pool,
drep,
stake: crate::model::epoch_value::testing::rebase(stake, epoch),
pool: crate::model::epoch_value::testing::rebase(pool, epoch),
drep: crate::model::epoch_value::testing::rebase(drep, epoch),
vote_delegated_at,
retired_pool,
}
Expand Down Expand Up @@ -1208,24 +1212,29 @@ mod prop_tests {
#[test]
fn stake_delegation_roundtrip(
entity in any_account_state(),
delta in any_stake_delegation(),
mut delta in any_stake_delegation(),
) {
// `apply` asserts alignment under `strict`, so the delta carries
// the entity's epoch.
delta.epoch = entity.pool.epoch().expect("generated at epoch position");
assert_delta_roundtrip(Some(entity), delta);
}

#[test]
fn vote_delegation_roundtrip(
entity in any_account_state(),
delta in any_vote_delegation(),
mut delta in any_vote_delegation(),
) {
delta.epoch = entity.drep.epoch().expect("generated at epoch position");
assert_delta_roundtrip(Some(entity), delta);
}

#[test]
fn stake_deregistration_roundtrip(
entity in any_account_state(),
delta in any_stake_deregistration(),
mut delta in any_stake_deregistration(),
) {
delta.epoch = entity.pool.epoch().expect("generated at epoch position");
assert_delta_roundtrip(Some(entity), delta);
}

Expand All @@ -1240,16 +1249,18 @@ mod prop_tests {
#[test]
fn pool_delegator_retire_roundtrip(
entity in any_account_with_active_pool(),
delta in any_pool_delegator_retire(),
mut delta in any_pool_delegator_retire(),
) {
delta.epoch = entity.pool.epoch().expect("generated at epoch position");
assert_delta_roundtrip(Some(entity), delta);
}

#[test]
fn drep_delegator_drop_roundtrip(
entity in any_account_state(),
delta in any_drep_delegator_drop(),
mut delta in any_drep_delegator_drop(),
) {
delta.epoch = entity.drep.epoch().expect("generated at epoch position");
assert_delta_roundtrip(Some(entity), delta);
}

Expand Down Expand Up @@ -1288,8 +1299,11 @@ mod prop_tests {
#[test]
fn account_transition_roundtrip(
entity in any_account_state(),
delta in any_account_transition(),
mut delta in any_account_transition(),
) {
// `transition` asserts `next_epoch` is exactly one past the
// entity's epoch under `strict`.
delta.next_epoch = entity.stake.epoch().expect("generated at epoch position") + 1;
assert_delta_roundtrip(Some(entity), delta);
}

Expand Down Expand Up @@ -1330,24 +1344,27 @@ mod prop_tests {
#[test]
fn stake_delegation_serde_roundtrip(
entity in any_account_state(),
delta in any_stake_delegation(),
mut delta in any_stake_delegation(),
) {
delta.epoch = entity.pool.epoch().expect("generated at epoch position");
assert_delta_serde_roundtrip(Some(entity), delta);
}

#[test]
fn stake_deregistration_serde_roundtrip(
entity in any_account_state(),
delta in any_stake_deregistration(),
mut delta in any_stake_deregistration(),
) {
delta.epoch = entity.pool.epoch().expect("generated at epoch position");
assert_delta_serde_roundtrip(Some(entity), delta);
}

#[test]
fn vote_delegation_serde_roundtrip(
entity in any_account_state(),
delta in any_vote_delegation(),
mut delta in any_vote_delegation(),
) {
delta.epoch = entity.drep.epoch().expect("generated at epoch position");
assert_delta_serde_roundtrip(Some(entity), delta);
}

Expand Down
23 changes: 23 additions & 0 deletions crates/cardano/src/model/epoch_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,29 @@ pub(crate) mod testing {
use crate::model::testing as root;
use proptest::prelude::*;

/// Rebase a generated `EpochValue` onto a specific epoch position,
/// keeping every slot as-is.
///
/// The `strict` feature asserts that deltas target the entity's current
/// epoch and that entities rotate their epoch values in lockstep (ESTART
/// transitions every entity each boundary). Strategies that draw several
/// `EpochValue`s — or an entity and a delta — independently violate that
/// invariant almost surely; rebasing them onto one drawn epoch restores
/// it without giving up randomized slot contents.
pub fn rebase<T>(value: EpochValue<T>, epoch: Epoch) -> EpochValue<T>
where
T: Clone + std::fmt::Debug,
{
EpochValue::from_parts(
epoch,
value.live().cloned(),
value.next().cloned(),
value.mark().cloned(),
value.set().cloned(),
value.go().cloned(),
)
}

/// Generate an `EpochValue<T>` where `live` is always populated (most
/// deltas require it) and the other slots are independently randomized.
///
Expand Down
23 changes: 18 additions & 5 deletions crates/cardano/src/model/epochs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,11 +403,14 @@ pub(crate) mod testing {
(0u32..32u32, 1u32..=32u32).prop_map(|(committed, total)| ShardProgress { committed, total })
),
) -> EpochState {
// ESTART rotates rolling/pparams in lockstep with the epoch
// number, so a healthy entity has all three aligned. The `strict`
// feature asserts this; rebase the independently drawn values.
EpochState {
number,
initial_pots,
rolling,
pparams,
rolling: crate::model::epoch_value::testing::rebase(rolling, number),
pparams: crate::model::epoch_value::testing::rebase(pparams, number),
largest_stable_slot,
previous_nonce_tail,
nonces,
Expand Down Expand Up @@ -1519,11 +1522,13 @@ mod prop_tests {
end in prop::option::of(any_end_stats()),
incentives in prop::option::of(any_epoch_incentives()),
) -> EpochState {
// Same lockstep rebase as `any_epoch_state`: `strict` asserts the
// epoch values sit at the entity's current epoch.
EpochState {
number,
initial_pots,
rolling,
pparams,
rolling: crate::model::epoch_value::testing::rebase(rolling, number),
pparams: crate::model::epoch_value::testing::rebase(pparams, number),
largest_stable_slot,
previous_nonce_tail,
nonces,
Expand Down Expand Up @@ -1729,8 +1734,12 @@ mod prop_tests {
#[test]
fn epoch_stats_update_roundtrip(
entity in any_epoch_state_no_rolling_next(),
delta in any_epoch_stats_update(),
mut delta in any_epoch_stats_update(),
) {
// `apply` mutates `rolling` through `live_mut`, which asserts
// alignment under `strict`, so the delta carries the entity's
// epoch.
delta.epoch = entity.rolling.epoch().expect("generated at epoch position");
assert_delta_roundtrip(Some(entity), delta);
}

Expand Down Expand Up @@ -1791,6 +1800,9 @@ mod prop_tests {
// align new_pots with the entity's initial_pots so apply's max_supply
// consistency debug_assert holds.
delta.new_pots = entity.initial_pots.clone();
// `transition` asserts `new_epoch` is exactly one past the
// entity's epoch under `strict`.
delta.new_epoch = entity.rolling.epoch().expect("generated at epoch position") + 1;
assert_delta_roundtrip(Some(entity), delta);
}

Expand All @@ -1802,6 +1814,7 @@ mod prop_tests {
// align new_pots with the entity's initial_pots so apply's max_supply
// consistency debug_assert holds.
delta.new_pots = entity.initial_pots.clone();
delta.new_epoch = entity.rolling.epoch().expect("generated at epoch position") + 1;
assert_delta_roundtrip(Some(entity), delta);
}

Expand Down
14 changes: 12 additions & 2 deletions crates/cardano/src/model/pools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -550,8 +550,14 @@ mod prop_tests {
#[test]
fn pool_registration_roundtrip(
entity in prop::option::of(any_pool_state()),
delta in any_pool_registration(),
mut delta in any_pool_registration(),
) {
// For existing pools `apply` schedules or replaces the snapshot,
// both of which assert alignment under `strict`, so the delta
// carries the pool's epoch.
if let Some(entity) = &entity {
delta.epoch = entity.snapshot.epoch().expect("any_pool_state fills a concrete epoch");
}
assert_delta_roundtrip(entity, delta);
}

Expand Down Expand Up @@ -585,8 +591,12 @@ mod prop_tests {
#[test]
fn pool_transition_roundtrip(
entity in any_pool_state(),
delta in any_pool_transition(),
mut delta in any_pool_transition(),
) {
// `transition` asserts `next_epoch` is exactly one past the
// snapshot's epoch under `strict`.
delta.next_epoch =
entity.snapshot.epoch().expect("any_pool_state fills a concrete epoch") + 1;
assert_delta_roundtrip(Some(entity), delta);
}
}
Expand Down
Loading