Changes in 0.7.0
- feat(contributor-rewards): the worker retries a failed merkle root posting 3 more times, one minute apart, before it reports the failure. The 3 retries share one grace-period wait, so
grace_period_max_wait_secondsstill limits the total wait - feat(contributor-rewards): the metro prices, which set the priority of the multicast demands, come from the new
metro_pricessettings and no longer from theMetroHistoryaccounts of the shred subscription program. The defaults are 100 for FRA and AMS, 60 for NYC, LON, SIN and TYO, and 30 for every other city, and the snapshot stores the prices of its epoch. Every city that is not in the table gets the default price, so a city whose price on chain was not 30 changes its Shapley share (malbeclabs/infra#3302) - feat(contributor-rewards): the settings
rpc.solana_read_urlandrpc.solana_write_urlbecome one setting,rpc.solana_url(DZ__RPC__SOLANA_URL), which the worker uses for reads and writes. Replace the two old settings with it. Use a Solana testnet URL whennetworkistestnet, and a mainnet-beta URL whennetworkismainnet-beta - feat(contributor-rewards): add
calculate-rewards --shapley-from-ledger, which reads the epoch's shapley output record from the DZ Ledger instead of running the Shapley calculation, then posts the merkle root built from that record. The flag implies--skip-device-telemetry,--skip-internet-telemetry,--skip-reward-inputand--skip-shapley-output, so no record on the ledger is rewritten and a failed merkle root posting retries without the 20 minute calculation (#4469) - feat(contributor-rewards): add
offchain/crates/contributor-rewards/Dockerfile, which builds the worker into alinux/amd64image for the Kubernetes CronJob. A person builds it from the repository root withdocker buildx build --platform linux/amd64 -f offchain/crates/contributor-rewards/Dockerfile .and pushes it to ECR (malbeclabs/infra#3302) - feat(contributor-rewards): the leader schedule of a DoubleZero epoch adds up the slot counts of every Solana epoch that the DoubleZero epoch overlaps, where it used the one Solana epoch that holds the first timestamp. Solana epochs are now shorter than DoubleZero epochs, so the one epoch described only the first part. The snapshot stores the epochs in
leader_schedule.solana_epochs, and an older snapshot without them still loads - feat(contributor-rewards): add
snapshot --leader-schedule-file, repeatable, which reads the leader schedule of one Solana epoch from a JSON file withsolana_epochandschedule_mapinstead of fetching it, and adds the files up. It makes a snapshot for an epoch that the RPC no longer holds a leader schedule for. The command refuses files that do not cover exactly the Solana epochs that the DoubleZero epoch overlaps - feat(contributor-rewards): add
scheduler start --once, which runs one tick and exits, with a failure code when a step of the tick failed, so a Kubernetes CronJob can run the scheduler and retry a failed run (malbeclabs/infra#3302) - feat(contributor-rewards): the scheduler checks the chain for an epoch's device telemetry, internet telemetry, reward input and shapley output records and its merkle root before it builds the snapshot, so a run with nothing to do ends without fetching the epoch. When a snapshot for the epoch is already in storage, it loads that snapshot instead of fetching again, so a retry posts the same root as the first attempt. To build a fresh snapshot, delete the stored one (malbeclabs/infra#3302)
- feat(contributor-rewards): with
access_key_idandsecret_access_keyboth empty or unset, the S3 client loads credentials from the AWS default credential chain, such as an instance role or a pod role. A configured pair works as before, and a config with only one of the two keys is an error (#4418) - fix(contributor-rewards): checking whether a snapshot exists in S3 returns false for a missing object. It matched "NotFound" in the error text, but the SDK prints only "service error", so a missing snapshot was an error
- feat(contributor-rewards): the scheduler creates the next epoch's
Distributionaccount each tick, moved fromdoublezero-solana-validator-debt initialize-distribution. Each tick finalizes the rewards of the epoch before the one it processes, which locks that root one epoch after it was posted. The program requires a finalized debt calculation first, so the scheduler finalizes it with zero debt in the same transaction. It skips the finalize step with a warning until the program'sminimum_epoch_duration_to_finalize_rewardsallows the epoch, so that setting must be 2 or lower. The rewards accountant key signs both steps, which is the debt accountant on mainnet-beta. The scheduler no longer writes off debt or sweeps tokens - feat(contributor-rewards): add the example
catch_up_distributions, which creates theDistributionof each DoubleZero epoch from the program's next epoch through--through-dz-epoch. The scheduler creates a distribution only when the program's next epoch equals the ledger's last completed epoch, so this example catches up when the program is behind. It waits for the program's initialization grace period between two creations, and it needs the debt accountant key. Run it withcargo run --release -p doublezero-contributor-rewards --example catch_up_distributions -- -um -k <KEYPAIR> --through-dz-epoch <EPOCH>, and add--dry-runto simulate the first creation - fix(contributor-rewards): the scheduler no longer pays each DoubleZero epoch's rewards from its
Distributionaccount once the swap program sweeps that epoch's tokens, and it no longer posts the Slack message for that payment. Thedistribute-rewardscommand and its Slack table are removed too. The monthly steps do not change. The state file dropslast_distributed_epochandconsecutive_distribution_failures, and a state file that still has them loads without error - fix(contributor-rewards): distributing rewards adds to its compute budget what the program spends deriving each recipient's ATA, 1,500 compute units for each bump past the first. Measured against the program, a recipient costs about 10,250 at its ATA's first bump, so the flat 12,500 per recipient ran out for a contributor whose recipients' ATAs derive at low bumps, and every retry failed the same way. Epoch and monthly payments share this budget (malbeclabs/infra#2696)
- feat(contributor-rewards): with
scheduler.enable_monthly_distributionset, which it is not by default, the scheduler finalizes the previous UTC month's contributor rewards and distributes the last three months'. Once the previous month is collected and a DoubleZero epoch has ended after it, the scheduler recalculates its shares, stores the leaves in amonthly_rewardsrecord on the DoubleZero Ledger keyed by month and merkle root, and posts and finalizes that root in one transaction. It distributes each unpaid leaf withDistributeMonthlyContributorRewardsfor three months, so a contributor that sets up its recipients late is still paid. Any other month with work left is for a manual run. Each tick warns about it, and a month left unfinalized also counts the tick as failed (malbeclabs/infra#2696) - feat(contributor-rewards): add
distribute-monthly-rewards --year --month --solana-url --dz-url [--dry-run], which finalizes and distributes one month by hand with the scheduler's own steps, signed with the rewards accountant's keypair. It takes both RPCs as flags and reads no config file. It covers a month the scheduler left for a manual run (malbeclabs/infra#2696, malbeclabs/infra#3211) - feat(contributor-rewards): with
scheduler.enable_monthly_distributionset, the scheduler records each DoubleZero epoch's end, the slot and block time of its last block, in anepoch_endrecord on the DoubleZero Ledger.calculate-monthly-rewardsreads these records before block times, so selecting a month no longer depends on the RPC's block history, which on testnet is about 18 days (malbeclabs/infra#2696) - feat(contributor-rewards): add
calculate-monthly-rewards, which averages the posted shares of every DoubleZero epoch whose last block falls in a UTC month into contributor shares that total exactly 1,000,000,000, the input monthly contributor distributions need. It refuses a month that has not closed, has no epochs, or has an epoch whose shapley output record does not match the merkle root posted to itsDistribution. It is read-only (malbeclabs/infra#2692) - test(contributor-rewards): mark the three keypair tests
#[serial]. They set and removeREWARDER_KEYPAIR_PATH, which is process-wide state, so one removing it while another sat betweenset_varand the read made the read fail. It never fired undercargo nextest, which gives each test its own process, and became reachable when this crate joined a workspace whose CI runscargo test - test(contributor-rewards): fix the Shapley golden tests.
assert_closenow treats both values as equal once each is below1e-6, so the seven per-city entries that are cancellation noise (values around1e-12) no longer sit below their own comparison gate.UPDATE_GOLDENnow regenerates only when set to1, not on any set value, so a stale exportedUPDATE_GOLDEN=0can no longer make both tests pass while silently overwriting the goldens. The fixture and goldens rename frommn-betatomainnet-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 withUPDATE_GOLDEN=1 cargo test -p doublezero-contributor-rewards - 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
ExpiredtoExpiredDeprecatedwhen 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 everyEpochFinderRPC 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::demandpath instead of silently mis-estimating (malbeclabs/infra#2317) - fix(contributor-rewards):
snapshotvalidates 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 asnapshotthenexport-shapleychain failed a step late. Pre-existing, but reachable now that resolving the Solana epoch depends on block-time reads (malbeclabs/infra#2317) - migrate to Solana 3.0: workspace
solana-*crates andsolana-sdkmove to the 3.0 line,solana-program-testto 3.0.12, and the doublezero SDK git-deps repin fromclient/v0.27.1to the #3830 merge revision (malbeclabs/infra#1853) - release artifact now builds as a static
x86_64-unknown-linux-muslbinary (malbeclabs/infra#1853) - 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)
- refactor(contributor-rewards): use the shared
Walletmemo helpers fromsolana-client-toolsin place of the localRELAY_MEMO_CUconstant - fix(contributor-rewards): backfill
User.feed_pk(zero pubkey) in the snapshot compat migration so snapshots captured before doublezero#4030 still deserialize (malbeclabs/infra#1853) - fix(contributor-rewards): handle a serviceability account that surfaces as a decode
Errper-type instead of aborting the whole fetch mid-loop. AccessPass (reward-neutral) decode failures are warned and skipped; every other, reward-bearing type fails the epoch loudly after warning each bad account, since a partial snapshot would feed Shapley a shrunk graph and freeze a skewed merkle. Decode failures increment aserviceability_decode_errorsmetric (labeled by account type) and are surfaced asDecodeErrors=in the completion log (#403)
Install
This release has no packages. Build the worker image from this tag with docker buildx build --platform linux/amd64 -f offchain/crates/contributor-rewards/Dockerfile . from the repository root.