Skip to content
This repository was archived by the owner on Sep 8, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ test-ledger/
.tool-versions
crates/contributor-rewards/dry-run-output/
crates/contributor-rewards/mainnet-beta.config.toml
!crates/contributor-rewards/tests/goldens/make-fixture.py
crates/solana-client-tools/target/
docs/plans
docs/superpowers
22 changes: 22 additions & 0 deletions crates/contributor-rewards/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- test(contributor-rewards): fix the Shapley golden tests. `assert_close` now treats
both values as equal once each is below `1e-6`, so the seven per-city entries that
are cancellation noise (values around `1e-12`) no longer sit below their own
comparison gate. `UPDATE_GOLDEN` now regenerates only when set to `1`, not on any
set value, so a stale exported `UPDATE_GOLDEN=0` can no longer make both tests pass
while silently overwriting the goldens. The fixture and goldens rename from
`mn-beta` to `mainnet-beta`, matching the no-abbreviations rule
- test(contributor-rewards): extend the Shapley golden to per-city outputs. The aggregate can hide drift when two cities move in opposite directions, so the per-city values are pinned as well
- test(contributor-rewards): pin the aggregated Shapley output for the committed mainnet-beta fixture with a golden file, the crate's first test covering reward values. Drives `PreparedData::from_snapshot`, the same path the scheduler uses for snapshots. Structure (operator set, ordering, counts) is asserted exactly. Values use a 1e-12 relative tolerance, because bit-identical floating point is not guaranteed across architectures and an exact gate would go permanently red on a CI architecture change. Regenerate deliberately with `UPDATE_GOLDEN=1 cargo test -p doublezero-contributor-rewards`
Comment thread
bgm-malbeclabs marked this conversation as resolved.
Outdated
- test(contributor-rewards): add a committed mainnet-beta snapshot fixture
(`tests/goldens/mainnet-beta-epoch-129-trimmed.json`) and the script that produces it
(`tests/goldens/make-fixture.py`), for a future Shapley golden test. The only
previously committed snapshot (testnet) produces zero reward for every operator, so
no golden test can assert a real value against it. Exact Shapley computation is
O(2^n) in the operator count, so the fixture keeps only the 4 contributors with the
most devices and the 6 cities with the most surviving devices among them, which
keeps the network connected and the computation fast. It is not a byte-size trim of
the full topology (malbeclabs/infra#2392)
- fix(contributor-rewards): migrate the access pass status value `Expired` to
`ExpiredDeprecated` when loading a snapshot captured before doublezero-serviceability
PR #3831 renamed that enum variant. Without the migration, such a snapshot fails to
deserialize (malbeclabs/infra#2392)
- fix(contributor-rewards): the scheduler no longer writes a snapshot it cannot use. A failed leader-schedule fetch was warned and discarded, so an unusable snapshot overwrote the epoch's canonical S3 key and the tick then failed reading it back with "Missing leader schedule". Both producers now propagate the fetch error, and the scheduler validates before saving, which also covers `--dry-run`, where nothing validated at all. Scheduler failures log the full cause chain, and every `EpochFinder` RPC error is stripped of its request URL, in the retry logs and in the error it propagates, since that URL carries the mainnet-beta read endpoint's API key into journald and Loki (malbeclabs/infra#2372)
- fix(contributor-rewards): resolve the Solana epoch for a timestamp from real block times instead of dividing wall clock by a hardcoded 400ms slot duration. The old estimate drifted about 30k slots per day of lookback and picked the wrong epoch near a boundary, and no fixed constant survives the SIMD-0525 rollout. That epoch selects the leader schedule rewards are computed against, so the search now errors rather than returning a wrong answer: a backfill older than the endpoint's ledger retention fails on the `ingestor::demand` path instead of silently mis-estimating (malbeclabs/infra#2317)
- fix(contributor-rewards): `snapshot` validates before writing. It warns and continues when the leader schedule cannot be fetched, but every consumer rejects a snapshot without one, so the command exited 0 having written an unusable file under the canonical name and a `snapshot` then `export-shapley` chain failed a step late. Pre-existing, but reachable now that resolving the Solana epoch depends on block-time reads (malbeclabs/infra#2317)
Expand Down
11 changes: 11 additions & 0 deletions crates/contributor-rewards/src/ingestor/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,17 @@ fn apply_serviceability_json_compat_migrations(serviceability: &mut Value) {
access_pass
.entry("max_multicast_users")
.or_insert_with(|| Value::Number(1.into()));

// doublezero-serviceability renamed the enum variant
// `AccessPassStatus::Expired` to `ExpiredDeprecated` (same discriminant,
// epoch expiry no longer demotes access passes). Snapshots serialized
// before that rename carry the old variant name.
Comment thread
martinsander00 marked this conversation as resolved.
Outdated
if access_pass.get("status").and_then(Value::as_str) == Some("Expired") {
access_pass.insert(
"status".to_string(),
Value::String("ExpiredDeprecated".to_string()),
);
}
}
}

Expand Down
Loading
Loading