Skip to content

Commit ef958fe

Browse files
authored
contributor-rewards, solana-cli, validator-debt: fix the hardcoded 400ms slot times (malbeclabs/doublezero-offchain#411)
## Summary of Changes Five call sites divided wall clock by a hardcoded 400ms slot duration. They get three independent fixes rather than one shared helper, because they want different answers: the two accuracy-critical sites stop depending on a slot-duration constant at all, and the two that keep one now share a single value. * **`contributor-rewards`** — `find_epoch_at_timestamp` verifies its estimate against real block times. The old estimate drifted ~30k slots per day of lookback and no fixed constant survives the SIMD-0525 rollout. That epoch picks the leader schedule rewards are computed against, so the search errors rather than guessing. * **`solana-cli`** — the duplicate constants in `shreds pay` and `prepare-offchain-message` collapse into one 350ms `NOMINAL_SLOT_DURATION`, deliberately cluster-independent: a `~` prefixed estimate and a deadline slot the CLI and operator must both compute want reproducibility over accuracy. * **`validator-debt`** — deletes two dead timestamp-to-epoch paths and both constants. `rpc.rs` already does this mapping correctly for production; the deleted code had no callers and contained an unsigned subtraction that panicked in debug and wrapped in release. ### Before merging * **`--valid-for` windows lengthen.** `--valid-for 1h` is now 10,285 slots, not 9,000. Mainnet reaches 350ms at epoch 1020 (2026-08-21); until then the flag grants ~68 minutes. Testnet runs at 200ms, so ~34 minutes. The help text states the conversion rate. * **The demand path can now fail where it used to be wrong.** `ingestor/demand.rs` propagates a leader-schedule error, so a backfill older than the endpoint's ledger retention errors instead of silently mis-estimating. The snapshot paths warn and continue, but `snapshot` now validates before writing rather than leaving an unusable file behind. * **350ms needs one more bump** when SIMD-0525 finishes stepping mainnet to 200ms. Nothing enforces it; the constants' comments record the schedule. ## Testing Verification * Pure-function tests for the search's per-step decision (both bounds, the unbounded current-epoch case, the unstarted-epoch case), the absent-block classifier (each of the three shapes `getBlockTime` uses, plus a pruned ledger failing closed), and the epoch-boundary skip budget (exclusive edge, a history gap, and a short-epoch cluster). No RPC mock, and no new dependency. * CLI assertions recomputed by hand from the 350ms derivation, with the arithmetic in each test comment. The deletions are checked by `clippy -Dwarnings`, which catches every orphaned import and unused constant.
1 parent 70a36ce commit ef958fe

12 files changed

Lines changed: 623 additions & 362 deletions

File tree

offchain/crates/contributor-rewards/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
- 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)
11+
- 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)
1012
- migrate to Solana 3.0: workspace `solana-*` crates and `solana-sdk` move to the 3.0 line, `solana-program-test` to 3.0.12, and the doublezero SDK git-deps repin from `client/v0.27.1` to the malbeclabs/doublezero#3830 merge revision (malbeclabs/infra#1853)
1113
- release artifact now builds as a static `x86_64-unknown-linux-musl` binary (malbeclabs/infra#1853)
1214
- TLS for HTTP clients moves from openssl to rustls; trust roots are the bundled webpki Mozilla set plus the host OS certificate store, so OS-installed private CAs remain trusted (malbeclabs/infra#1853)

offchain/crates/contributor-rewards/src/cli/snapshot.rs

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -252,31 +252,22 @@ pub async fn create_snapshot(
252252
fetcher.dz_rpc_client.clone(),
253253
fetcher.solana_read_client.clone(),
254254
);
255-
let solana_epoch = match epoch_finder
256-
.find_epoch_at_timestamp(fetch_data.start_us)
255+
// fetch_leader_schedule resolves the Solana epoch itself and reports which
256+
// one it used, so taking the epoch from its result avoids running the
257+
// chain-verified epoch search twice over the same timestamp.
258+
let leader_schedule = match epoch_finder
259+
.fetch_leader_schedule(fetch_epoch, fetch_data.start_us)
257260
.await
258261
{
259-
Ok(epoch) => Some(epoch),
262+
Ok(schedule) => Some(schedule),
260263
Err(e) => {
261-
warn!("Failed to determine Solana epoch: {}", e);
264+
warn!("Failed to get leader schedule: {}", e);
262265
None
263266
}
264267
};
265-
266-
let leader_schedule = if solana_epoch.is_some() {
267-
match epoch_finder
268-
.fetch_leader_schedule(fetch_epoch, fetch_data.start_us)
269-
.await
270-
{
271-
Ok(schedule) => Some(schedule),
272-
Err(e) => {
273-
warn!("Failed to get leader schedule: {}", e);
274-
None
275-
}
276-
}
277-
} else {
278-
None
279-
};
268+
let solana_epoch = leader_schedule
269+
.as_ref()
270+
.map(|schedule| schedule.solana_epoch);
280271

281272
// Create metadata
282273
let metadata = SnapshotMetadata {
@@ -327,6 +318,11 @@ pub async fn create_snapshot(
327318
Network::Devnet => "dn",
328319
};
329320

321+
// Refuse to write a snapshot no consumer can read. Without this the command
322+
// exits 0 having left an unusable file under the canonical name, and the
323+
// failure surfaces a step later in whatever reads it next.
324+
snapshot.validate()?;
325+
330326
// Export: local override or configured storage
331327
if local_file.is_some() || local_dir.is_some() {
332328
// Save to local filesystem (ignores storage backend config)

0 commit comments

Comments
 (0)