repo: import doublezero-offchain and doublezero-solana with their history - #4240
repo: import doublezero-offchain and doublezero-solana with their history#4240bgm-malbeclabs wants to merge 553 commits into
Conversation
# Summary Update changelogs.
Adds PR template same as doublezero repo
…n#116) ## Summary of Changes This PR adds prom metrics to validator-debt. The metrics endpoint is specified with an env var and if it's not set, then metrics aren't captured. Only a few things have been instrumented which are the most important and more might be added as needed. Closes #1723. ## Testing Verification Ran the validator-debt app locally and in a separate tab ran `curl` against the metrics endpoint to verify that metrics were being properly surfaced. ``` > curl localhost:9090/metrics # TYPE doublezero_validator_debt_overlapping_epochs counter doublezero_validator_debt_overlapping_epochs{dz_epoch="45",solana_epoch="871"} 1 # TYPE doublezero_validator_debt_build_info gauge doublezero_validator_debt_build_info{version="0.1.0-rc4",commit="UNKNOWN",date="UNKNOWN",pkg_version="0.1.0-rc4"} ```
…/doublezero-offchain#181) ## Summary of Changes This PR moves validator-debt in line with the [group_imports](https://rust-lang.github.io/rustfmt/?version=v1.8.0&search=#group_imports) that are on unstable rust. Unfortunately, this can't be accomplished with adding the directive to rustfmt because it's only on unstable so this will have to be something that is monitored through code review or another mechanism. Closes #2087. ## Testing Verification * Build still builds
…eclabs/doublezero-offchain#180) ## Summary of Changes This PR adds a command called `harvest-2z`, which requests a quote and swap instructions from Jupiter endpoints to atomically swap SOL into 2Z in order to convert SOL to 2Z for the Revenue Distribution program (by atomically buying SOL in conjunction with the Jupiter route). The round-trip SOL -> 2Z -> SOL pathway is protected by using the swap rate fetched by the SOL conversion oracle. There must be enough SOL in the user's wallet in order for this command to succeed. The command itself is gasless, where your SOL balance should be unchanged after execution. Once executed, the user will have harvested some amount of 2Z tokens. If the user does not currently have a 2Z associated token account, creating this token account will result in less 2Z harvested, but will still preserve your SOL balance after execution. To execute, run the following: ```sh doublezero-solana revenue-distribution harvest-2z ``` Output will resemble: ```sh Harvested 5.98151278 2Z tokens with 1.000000000 SOL ``` The command can also be simulated with the `--dry-run` argument. Dry-run will produce program logs and an output at the end resembling: ```sh Simulated harvesting 5.98151278 2Z tokens with 1.000000000 SOL ``` ## Testing Verification Performed the command with dry-run mode.
…` argument (malbeclabs/doublezero-offchain#182) ## Summary of Changes This PR adds more view modes for distribution accounting. These views help give a better view of all of the accounting that goes into the lifecycle of a distribution. These views fetch the records posted by the protocol's accountant processes and exposes how the distribution tracks this data (e.g. how much a contributor is rewarded and whether its rewards were distributed). - Add `--view` argument with the following values: - `summary` -- Default view. Shows aggregate information about fee configuration, debt and rewards. - `validator-debt` -- Record of all Solana validator debt owed and whether it has been processed. - `unprocessed-validator-debt` -- Similar to `validator-debt` but only shows unprocessed debt. - `rewards` -- Record of all network contributors reward distributions. - Cleaned up default fetch (now `summary` view) for distributions. Examples of these views will be posted as comments. This change also affected public interfaces to the `doublezero-solana-validator-debt` and `doublezero-contributor-rewards` crates, which is why I also updated these CHANGELOGs. Closes #2035. ## Testing Verification * Tested views locally (examples will be in the comments).
…ero-offchain#184) # Summary Fix https://github.com/malbeclabs/infra/issues/346 Fixes connection reset retry logic that was added in malbeclabs/doublezero-offchain#177 but didn't work properly. - The previous fix attempted to detect "connection reset" errors by checking if `reqwest_err.to_string().contains("connection reset")`, but this never matched because `Display` for `Kind::Request` only outputs `"error sending request"` without the nested source details - This caused connection reset errors to bypass retry logic and immediately log at ERROR level, triggering Grafana alerts - This implementation walks the error source chain (following the same pattern as `reqwest::Error::is_timeout()`, which you can refer [here](https://github.com/seanmonstar/reqwest/blob/a97e1956dd14a79b0207082e327098331519bf2b/src/error.rs#L116)) to find the underlying `io::Error` and check for `io::ErrorKind::ConnectionReset`
…bin (malbeclabs/doublezero-offchain#185) ## Summary of Changes * Change installation directory for binaries from /usr/local/bin/ to /usr/bin/ * Linux distributions require that packages do not install files into /usr/local/: * https://www.debian.org/doc/debian-policy/ch-opersys.html#site-specific-programs * https://docs.fedoraproject.org/en-US/packaging-guidelines/ Closes #2083. ## Testing Verification * Built packages locally with `goreleaser --snapshot --clean`
…clabs/doublezero-offchain#187) ## Summary of Changes * Binaries were moved from /usr/local/bin/ to /usr/bin/ in a previous PR * This PR updates changelogs and systemd units to reflect this change
…albeclabs/doublezero-offchain#186) # Summary Adds grace period checking before posting contributor rewards merkle roots to prevent on-chain transaction failures. The scheduler now waits for the Revenue Distribution program's grace period (default: 1 hour) to expire before attempting to post calculations. Fixes #2118 ## Changes - Add `check_calculation_allowed()` to validate grace period based on Distribution timestamp - Add `wait_for_grace_period()` to poll every 60s until grace period expires - Update `post_rewards_merkle_root()` to wait for grace period before posting - Add `grace_period_max_wait_seconds` configuration (default: 6 hours) - Update all tests to include new configuration field
…a-validator-debt (malbeclabs/doublezero-offchain#188) ## Summary of Changes * Release new versions of contributor-rewards, sentinel, and solana-validator-debt
…clabs/doublezero-offchain#189) ## Summary of Changes - Remove the default Jupiter route to allow any route from SOL -> 2Z. - Add `--specific-dex` optional argument to allow the user to specify one DEX program label. For example: ```sh doublezero-solana revenue-distribution harvest-2z --specific-dex HumidiFi ``` See https://lite-api.jup.ag/swap/v1/program-id-to-label for reference. ## Testing Verification Tested command with `--dry-run`.
…rvest-2z` (malbeclabs/doublezero-offchain#190) ## Summary of Changes The default limit price was not factoring the discounted price. This change adds this, which makes the atomic swap in `harvest-2z` safer. Also added a check in `harvest-2z` in the simulation to ensure that the token balance change increases. ## Testing Verification Executed `convert-2z` and `harvest-2z` with `--dry-run`.
## Summary of Changes Uptick crate version to 0.2.2 to prepare for release. ## Testing Verification N/A
…ublezero-solana#86) We do not want the system to finalize a null rewards root if there is revenue to distribute for a given epoch. Presumably there is revenue because users are using the network, so there should be contributors to reward. Closes #2098.
…/doublezero-offchain#195) ## Summary of Changes Companion PR to malbeclabs/doublezero-solana#87. Whenever the Revenue Distribution program is upgraded to take this change, admin needs to call the migration instruction after the upgrade, which will fix the Journal state. This command needs to reflect the new way the migration instruction must be called. Closes #2144. ## Testing Verification CI in malbeclabs/doublezero-solana#87 forks mainnet to test this CLI command.
…ero-solana#87) This change requires performing a migration that fixes the Journal's balance after an onchain upgrade. Closes #2097.
…lezero-offchain#183) ## Summary of Changes This PR adds a new app called `scheduler` to manage the debt lifecycle processes. It's written in Elixir which is a new language and adds some risk as only Rahul and I know Elixir well. However, this is done not simply to increase complexity but because it provides a lightweight and reliable scheduling mechanism through Erlang's process supervision. This could be written in Rust but it would take longer and be less reliable. It might be worth rewriting in Rust at some point. It'd be pretty simple as the surface area is intentionally kept as small as possible. It uses the same entrypoint as the CLI so there aren't competing implementations. Rust is interfaced through a NIF (native implemented function) which is like foreign function interface (FFI) in other languages. There's a thin layer written in Rust that calls into the `validator-debt` crate et al. The way this works is that there is an application [supervisor](https://hexdocs.pm/elixir/Supervisor.html) that manages processes. Any of the lifecycle processes will be managed through a [GenServer](https://hexdocs.pm/elixir/1.19.2/GenServer.html) which basically runs in a loop executing some component of the debt lifecycle process at some interval. Intervals are configured using cron syntax. `CHANGELOG.MD` added. Note there is some followup work to extract some of the logic from the CLI into a shared place, perhaps `solana-tools` but that will be done in a subsequent PR. Closes #2029 ## Testing Verification This has been tested running every two hours with results being posted to slack as seen [here](https://malbeclabs.slack.com/archives/C09LES1Q127). The app was run from my local machine for more than an day without interruption or errors.
…bs/doublezero-offchain#196) # Summary This PR replaces the point-in-time access pass validator selection approach with the canonical time-series approach that matches the Python script used for [fees](https://github.com/doublezerofoundation/fees/). Fetches validator pubkeys from S3 hourly Parquet snapshots and applies the 12-hour connection rule (validators must appear in >12 hourly snapshots to qualify). Fixes #2039 ## Changes - **New `s3_fetcher` module** - Fetches hourly Parquet snapshots from S3 for a given Solana epoch - Merges gossip, validators, users, and devices datasets - Applies 12-hour connection rule (>12 appearances required) - Supports mainnet-beta and testnet (probably just remove testnet?) - Implements consecutive failure tracking (default 12, configurable) - **Updated `worker.rs`** - Integrated S3 fetcher to replace access pass approach - Added CSV validation logic for testing (strict 100% match required) - Removed deprecated access pass code - Now uses S3-fetched validators for reward calculations - **New `export-validators` command** - CLI: `solana-validator-debt export-validators --epoch <EPOCH> [--output <CSV>]` - Exports validator pubkeys per epoch to CSV format - Mostly for validation and debugging - **Required Env Vars:** - `VALIDATOR_DEBT_AWS_ACCESS_KEY_ID` - AWS access key - `VALIDATOR_DEBT_AWS_SECRET_ACCESS_KEY` - AWS secret key - **Optional Env Vars:** - `VALIDATOR_DEBT_S3_BUCKET` (default: "malbeclabs-data-metrics-dev") - `VALIDATOR_DEBT_AWS_REGION` (default: "us-east-1") - `VALIDATOR_DEBT_S3_MAX_CONSECUTIVE_FAILURES` (default: 12) - `VALIDATOR_DEBT_S3_ENDPOINT` (for MinIO compatibility) ## Testing ``` +---------+----------+--------+----------+-------------+---------------+ | Epoch | Status | Rust | Python | Rust Only | Python Only | |---------+----------+--------+----------+-------------+---------------| | 859 | ✓ | 380 | 380 | 0 | 0 | | 860 | ✓ | 385 | 385 | 0 | 0 | | 861 | ✓ | 390 | 390 | 0 | 0 | | 862 | ✓ | 397 | 397 | 0 | 0 | | 863 | ✓ | 397 | 397 | 0 | 0 | | 864 | ✓ | 401 | 401 | 0 | 0 | | 865 | ✓ | 407 | 407 | 0 | 0 | | 866 | ✓ | 411 | 411 | 0 | 0 | | 867 | ✓ | 411 | 411 | 0 | 0 | | 868 | ✓ | 410 | 410 | 0 | 0 | | 869 | ✓ | 406 | 406 | 0 | 0 | | 870 | ✓ | 399 | 399 | 0 | 0 | | 871 | ✓ | 400 | 400 | 0 | 0 | | 872 | ✓ | 403 | 403 | 0 | 0 | | 873 | ✓ | 403 | 403 | 0 | 0 | | 874 | ✓ | 382 | 382 | 0 | 0 | | 875 | ✓ | 385 | 385 | 0 | 0 | | 876 | ✓ | 387 | 387 | 0 | 0 | | 877 | ✓ | 387 | 387 | 0 | 0 | | 878 | ✓ | 385 | 385 | 0 | 0 | +---------+----------+--------+----------+-------------+---------------+ ``` ## TODO - [x] Validate results against more CSV files from [fees](https://github.com/doublezerofoundation/fees) - [x] Remove debugging - [x] Remove CSV validation logic - [x] Remove CSV path parameter from `calculate_validator_debt` - [x] Perhaps remove "access pass blocked" bail entirely --------- Co-authored-by: Ben Marx <bgm@malbeclabs.com>
…ler (malbeclabs/doublezero-offchain#197) ## Summary of Changes This PR adds in a worker to manage initializing distributions automatically. It runs every two minutes. This PR also moved some of the logic for initializing a distribution into the validator-debt worker so that both the scheduler and the CLI have a common, minimal interface into the underlying code. Closes #2141. ## Testing Verification The current distribution has already been initialized but here's evidence of the worker calling the underlying rust code: ``` 14:02:00.650 [debug] Scheduling job for execution 14:02:00.658 [debug] Task for job started on node 14:02:00.658 [debug] Execute started for job 14:02:00.760 [debug] Execution ended for job 14:02:01.328 [error] initialize_distribution: received error: "Last completed DZ epoch 52 != program's epoch 53" 14:02:01.331 [error] GenServer Scheduler.Worker.InitializeDistribution terminating ** (stop) "initialize_distribution shutting down" Last message: {:continue, :initialize_distribution} ```
…/doublezero-offchain#198) # Summary contributor-rewards scheduler was entering a permanent failure state after 10 consecutive RPC failures, requiring manual intervention to recover. This PR removes the hard halt and implements self-healing retry logic with monitoring alerts. Fixes https://github.com/malbeclabs/infra/issues/368 ## Changes - Remove hard halt: Scheduler now retries indefinitely instead of halting after max failures - Add RPC retry with backoff: Wrap get_epoch_info() with backoff - Add periodic alerting: Emit ERROR log every 10 consecutive failures for monitoring - Remove max_consecutive_failures config: No longer needed since hard halt is removed
…albeclabs/doublezero-solana#88) Open for comments. Closes #2093.
…ro-offchain#199) ## Summary of Changes This PR adds in a worker to automatically run calculate_distribution. It follows the existing pattern of having a light interface into the underlying validator-debt crate and runs every two hours. It works by getting the current dz_epoch, then subtracting one epoch to get the most recently completed epoch. If the epoch is already finalized, the worker just shuts down. Otherwise it continues its run as usual. In a subsequent PR, we'll add a counter to this that must be reached before the debt is finalized. Note that due to the differences in floating point math (how "society" rounds - round 5 up) and scalar math (safer with integers - floor) there can be a 1 lamport difference between Nihar's CSV and the calculations provided by the `mul_scalar/1` method that Karl created. We've verified for every epoch that there's no greater than 1 lamport difference between the csv and the calculations in the validator-debt crate. Closes #2027 ## Testing Verification This has been tested and results posted in [#tmp-validator-debt](https://malbeclabs.slack.com/archives/C09LES1Q127/p1763490621115539)
…lezero-offchain#200) ## Summary of Changes This PR updates the calculate_distribution worker to add in a counter that increments each time the calculate_distribution function successfully completes. When the counter reaches three, it moves to the finalize_distribution callback where the finalize_distribution NIF is called. If any of the calls fail or the computed_debt differs from the ledger debt record, the process is stopped and the counter is reset to zero. Note that only the third successful run is posted to slack. Closes #2028 ## Testing Verification * This was run and posted to slack as evidenced [here](https://malbeclabs.slack.com/archives/C09LES1Q127/p1763585298695259)
…mmand (malbeclabs/doublezero-offchain#201) ## Summary of Changes Add Solana validator debts view to `doublezero-solana` CLI. There are two `--view` modes: `outstanding` and `node`. Outstanding has an optional `--node-id` parameter to calculate the total outstanding debt for a given validator node ID. If unspecified, the command will show all validators with outstanding debt. The node view shows all debt statuses for every relevant Solana epoch for a given `--node-id`. Other changes include: - Remove `fetch journal` from CLI. - Remove `--dz-ledger-url` where the specific DoubleZero Ledger connection can be derived from the Solana connection. - Better account fetching and account data processing in Solana tools. - Refactor CLI based on tools changes. - Add `debt_record_key` to `doublezero-solana-validator-debt` crate. - Specify minor versions of zero-versioned dependencies and removed unused dependencies from workspace. Closes #2045. ## Testing Verification Ran new and existing fetch commands locally.
…otency (malbeclabs/doublezero-offchain#202) # Summary Fixes #2199 The idempotency check only verified on-chain records (steps 1-4) but didn't check if the merkle root was posted to the Distribution account (step 5). This caused epochs to be incorrectly marked as processed when only partial work was completed. Changes: - Add check_distribution_merkle_root() method to verify Distribution account - Update rewards_exist_for_epoch() to require both records AND merkle root - Add retry logic with exponential backoff for Distribution account fetch The scheduler will now retry epochs indefinitely until ALL conditions are met: - Records exist (shapley output or reward input) - Distribution account exists for that epoch - Distribution.rewards_merkle_root != Hash::default() (non-zero) - Distribution.total_contributors > 0
…clabs/doublezero-offchain#203) # Summary Add 5 individual skip flags to the calculate-rewards command to allow selective execution of write operations, providing more flexibility than the all-or-nothing --dry-run flag. Fixes: #2206 This enables scenarios like: - Reposting only merkle root without rewriting telemetry - Updating specific ledger records while preserving others - Testing individual write steps in isolation - Recovery from partial failures with selective re-execution ## Changes - Add `WriteConfig` struct with tests - Add `--skip-device-telemetry` flag for device telemetry write - Add `--skip-internet-telemetry` flag for internet telemetry write - Add `--skip-reward-input` flag for reward input write - Add `--skip-shapley-output` flag for shapley output storage write - Add `--skip-merkle-root` flag for merkle root posting to Solana
…albeclabs/doublezero-offchain#204) ## Summary of Changes ## Testing Verification * Ran it locally with [act](https://github.com/nektos/act) * Doing it live
…albeclabs/doublezero-offchain#206) # Summary This PR adds Slack webhook integration to notify when contributor rewards cycles complete, with detailed tracking of all write operations and their on-chain identifiers. Fixes: #2211 ## Changes - Add `SlackSettings` to configuration with webhook URL and channel ID - Extend `WriteSummary` to track identifiers (record addresses/transaction signatures) for each write operation - Update `post_rewards_merkle_root()` to return transaction signature instead of () - Update `calculate_rewards()` to return `WriteSummary` with all operation results - Add `slack-notifier::contributor_rewards` module with markdown table formatting - Integrate Slack notifications into scheduler worker (automatic after successful completion) - Add `--slack-notify` CLI flag for testing notifications with `calculate-rewards` command - Add Slack settings validation (webhook URL format, required fields)
…o-offchain#2193) (malbeclabs/doublezero-offchain#205) ## Summary of Changes This PR adds goreleaser and actions so that the scheduler can be deployed to remote environments Closes #2193 ## Testing Verification * This has been deployed to both testnet and mainnet-beta and the binary is in [cloudsmith](https://cloudsmith.io/~malbeclabs/repos/doublezero/packages/detail/deb/doublezero-offchain-scheduler/0.1.0-1/a=amd64;xc=main;d=any-distro%252Fany-version;t=binary/)
…albeclabs/doublezero-offchain#397) ## Summary of Changes * Workspace `solana-*` crates and `solana-sdk` move to the 3.0 line (`solana-program-test` 3.0.12, `solana-loader-v3-interface` 6, `svm-hash` 0.2, spl interface crates on their v2 lines); doublezero SDK git-deps repin from `client/v0.27.1` to the #3830 merge revision `d2824391` (swap for the next v3 `client/*` tag once cut). * Source adaptations for removed `solana_sdk` re-exports: `solana-sdk-ids`, `solana-compute-budget-interface`, `solana-commitment-config`, `solana-reward-info`, `solana-address-lookup-table-interface`, boxed `ClientErrorKind`, `UiTransactionError` in simulation results, `SysvarSerialize` bounds. All verified semantics-preserving. * reqwest moves to `rustls-tls` (drops `openssl-sys` from the runtime graph); the CLI and three daemons release as static `x86_64-unknown-linux-musl` binaries so they run on older glibc hosts, mirroring #3830. TLS trust roots change from the OS store to the bundled webpki set (changelogged). * The scheduler NIF stays on the host gnu toolchain (ships inside the Elixir release, not as a cargo artifact). * Completes the offchain half of the V3 upgrade (malbeclabs/infra#1851); unblocks workspace unification (malbeclabs/infra#1856). * Fixes malbeclabs/infra#1853 ## Testing Verification * New `rust-cli-static` CI job builds the CLI for musl and asserts `file` reports statically linked; `rust-toolchain.toml` now provisions the musl target and `release/Dockerfile.release` gains `musl-tools`/`cmake` so `scripts/release-rc.sh` snapshot builds keep working. * `cargo tree` confirms `openssl-sys`/`native-tls` are gone from the runtime dependency graph (openssl remains only via the dev-only `solana-program-test` path). * Four independent reviews (architecture, security, release-pipeline, SDK) traced every rewritten RPC error-classification path against the Solana 3.0.12 sources and `origin/main`; no critical/high findings, and the one major (RC-snapshot Docker image missing the musl toolchain) is fixed here. * Fork/e2e coverage relies on the `local-validator` workflow on this branch (already runs against a Solana v3.0.12 runtime).
…abs/doublezero-offchain#383) ## Reviewing this PR History is organized into **3 commits by area** — review by commit: 1. **core plumbing + passport** — the RFC-20 `execute(&ctx, …)` contract, the `CliContext` + global-flags wiring in `main.rs`, the per-verb→ctx merge helpers (`build_wallet`, `solana_connection`, `resolve_network_env`), payer plumbing, and the passport verbs as the exemplar. The behavior-relevant commit — review closely. 2. **shreds verbs** — convert to the new signature and route each verb's per-verb connection/keypair override through the ctx merge helpers. Also includes the `publisher-rewards status` verb (landed on main in malbeclabs/doublezero-offchain#381) converted to the contract, and the `shreds --dz-ledger-url` subcommand-position flag retained as before. 3. **revenue-distribution verbs** — same conversion (incl. the in-place `relay` subtree, which keeps `SolanaPayerOptions` for the `Schedulable` trait pending malbeclabs/doublezero-offchain#1520). Note: commits 1–2 don't individually compile by design (the dispatch lands in commit 1; the verbs it calls are converted in 2–3) — review cumulatively; the PR head is what builds. **Rebased onto Solana v3 `main`** (#3830 + offchain malbeclabs/doublezero-offchain#397). Because `main` had independently rewritten several shreds verbs, `claim`, `list`, `withdraw`, and `status` were re-derived from main's current v3 bodies with the RFC-20 transform applied (not the pre-v3 bodies). Connections resolve to the network's Solana L1 for all monikers, matching malbeclabs/doublezero-offchain#396's removal of the testnet DZ-Ledger special-case (reach the testnet program with `-u devnet`). --- Parent Issue for Migration Process: malbeclabs/infra#1515 ## Summary of Changes * Refactor all CLI verbs under `crates/solana-cli/src/command/` to follow the RFC-20 verb contract: each verb is now `async fn execute(self, ctx: &CliContext, out: &mut impl Write) -> Result<()>`. * Add global flags (`--env`, `--solana-url`/`--url`, `--dz-ledger-url`, `--keypair`) to the `doublezero-solana` binary, constructing a `CliContext` at startup via `CliContextBuilder`. * **Backwards compatible — no behavior change without the new global flags.** Per-verb `--url`/`-u`, `--keypair`/`-k`, `--dz-env`, and `shreds --dz-ledger-url` keep their positions and their meaning, including everything the per-verb moniker implies (network environment, token mints, oracle key, serviceability program id, DZ Ledger URL) — resolved per-verb from the moniker or the connection's genesis hash exactly as before (`resolve_network_env`). The global flags supply defaults; per-verb flags win when present. * **Global `--env` uses the DoubleZero environment taxonomy** (`mainnet-beta`, `testnet`, `devnet`, `local`), matching the `doublezero` CLI these verbs will mount into (malbeclabs/doublezero-offchain#1520): `--env devnet` selects the DZ devnet environment (Solana L1 = testnet), with every unset context field derived from `env.config()`. The Solana devnet cluster (e.g. the testnet shred-subscription program per malbeclabs/doublezero-offchain#1763) stays reachable with a trailing `-u devnet`. * `WriteVerbOptions` carries each write verb's per-verb connection, keypair, and signer flags; `build_wallet` merges those over the `CliContext`. Read verbs keep a per-verb `SolanaConnectionOptions` merged the same way (`solana_connection`). Passport and the relay subtree keep their existing per-verb options unchanged. * Replace `println!` with `writeln!(out, ...)` across all verbs for testability with `Vec<u8>` writers. * Add `Wallet::write_verbose_output` writer-aware variant in `solana-client-tools`. * This unblocks malbeclabs/infra#1520 (mount into unified `doublezero` binary). * Fixes malbeclabs/infra#1517 ## Testing Verification * All 161 tests pass (145 lib + 16 binary) on Solana v3, including the trailing-flag back-compat tests (`-u`/`-k`/`--dz-env` after the subcommand on read, write, and shreds verbs), the `merge_ctx` consistency tests, and the `--env` taxonomy tests; `test_command_tree_is_valid` confirms no clap argument collisions. * `cargo clippy --all-targets` clean (no warnings); `cargo fmt` applied. * Architecture and security reviews posted to malbeclabs/infra#1517.
…in#4030 (8b795a3) (malbeclabs/doublezero-offchain#399) ## Summary - Bump all seven `malbeclabs/doublezero` crate pins from the malbeclabs/doublezero-offchain#3830 Solana 3.0 boundary (`d2824391`) to the malbeclabs/doublezero-offchain#4030 merge (`8b795a3c`) — 42 commits forward on the same Solana 3.0 line. malbeclabs/doublezero-offchain#4030 widens the EdgeSeat `FeedSeat` with per-feed billing state and adds the `SetAccessPassFeeds` instruction. - Unblocks downstream repos consuming `doublezero-solana-client-tools` to resolve a single doublezero source at malbeclabs/doublezero-offchain#4030 (cargo treats differing revs of the same repo as distinct sources, so client-tools and the consumer's direct pins must agree). Immediate consumer: the shred-subscription oracle's access-pass-expiry cranker, which needs the `FeedSeat` / `SetAccessPassFeeds` types. - Same `0.29.0` crate versions as malbeclabs/doublezero-offchain#3830 — no version bump, no offchain source changes. ## Testing - `cargo build --workspace` clean (2m12s, 0 errors): solana-cli, solana-client-tools, contributor-rewards, scheduler, validator-debt, and passport-cli all compile against malbeclabs/doublezero-offchain#4030 unmodified.
…on (malbeclabs/doublezero-offchain#400) ## Summary - malbeclabs/doublezero-offchain#4030 appended `feed_pk` to the serviceability `User`. The Borsh path defaults it for absent tails, but the JSON snapshot path (serde) has no default, so pre-malbeclabs/doublezero-offchain#4030 snapshots fail with `missing field feed_pk` — this turned `main` red after malbeclabs/doublezero-offchain#399. - Backfill `feed_pk` (zero pubkey) in `apply_serviceability_json_compat_migrations`, alongside the existing `tunnel_*`/`bgp_*` tail defaults. ## Testing - `cargo test -p doublezero-contributor-rewards --test test_demand --test test_pub_links` — 6/6 pass (the 4 previously-failing `feed_pk` tests plus 2 siblings).
…malbeclabs/doublezero-offchain#398) Refs: malbeclabs/infra#1807 ## Summary - Adds `doublezero-solana shreds pay --operator-key <PUBKEY>`, letting a buyer designate a DoubleZero operational identity (the eventual `user.owner` / access-pass `user_payer`) distinct from the escrow's withdraw (finance) authority. This is the buyer-facing half of the operator-key split (PR3 of three: onchain program → oracle → this CLI) for infra#1807, where a customer cannot publish to a private multicast group because their oracle-provisioned shred-seat user is owned by the funding authority rather than their operational identity. - Carries the operator key on the hand-mirrored `InitializePaymentEscrow` instruction (this repo maintains its own copy of the shred-subscription wire format). Serialization writes `discriminator + 32-byte key`; deserialization treats the key as an **optional trailing argument** (defaults when absent) so `shreds payments` continues to decode transactions that predate the field. - The flag is optional and defaults to the payer wallet, which reproduces today's behavior exactly (owner = withdraw authority). The operator key is instruction data, so the accounts list is unchanged. ## Testing - SDK round-trip test for the new tuple form, plus a legacy-wire-form test proving a discriminator-only (pre-field) transaction still decodes (to the default key) — the guarantee `shreds payments` history decoding relies on. - `cargo test -p doublezero-solana-sdk` (40 pass, incl. the two new tests) and `cargo test -p doublezero-solana-cli` (green); `just clippy` and `just fmt-check` clean; `shreds pay --help` lists `--operator-key`. ## Deploy / rollout - This CLI must ship **only after** the onchain program change (the shred-subscription `InitializePaymentEscrow` optional-arg upgrade) is deployed. The new CLI unconditionally appends the 32-byte operator key; the currently-deployed program parses instruction data with `try_from_slice`, which rejects trailing bytes, so a new CLI against the un-upgraded program would fail escrow initialization. The upgraded program accepts both old and new CLIs. This is the one hard ordering constraint for the coordinated rollout.
…eclabs/doublezero-offchain#401) Refs: malbeclabs/infra#1807 Backs out the buyer-CLI half of the operator-key split, which is being replaced by a simpler allowlist-based approach (see the doublezero-shreds revert). Coordinated with that PR. ## Summary - Reverts malbeclabs/doublezero-offchain#398: the `--operator-key` flag on `shreds pay` and the operator-key argument in the `solana-sdk` `InitializePaymentEscrow` builder. ## Deploy / rollout - Ship together with the doublezero-shreds revert: the reverted `shred-subscription` program rejects the trailing operator-key bytes this CLI appended, so a non-reverted CLI against the reverted program would fail escrow creation.
…oublezero-offchain#402) ## Summary - Bump the malbeclabs/doublezero SDK-family pins (`doublezero-cli-core`, `doublezero-config`, `doublezero-program-common`, `doublezero-record`, `doublezero_sdk`, `doublezero-serviceability`, `doublezero-telemetry`) from the interim malbeclabs/doublezero-offchain#4030 merge-commit rev (`8b795a3c`) to the released tag `client/v0.31.0` (`25ae6ee7`) — the swap the pin comment planned for "the next v3-containing client/* tag once one is cut". - This unblocks the do-tn-rewards1 incident: contributor-rewards v0.6.1 crash-loops with `memory allocation of 52264173600 bytes failed` because its `client/v0.27.1`-pinned SDK cannot decode the EdgeSeat access pass created on testnet 2026-07-22T14:46:30Z (`5x9DTsWCrAQtYKH4W7trEjoaCRHnzywsZp2oFX3sg5Zg`, `EdgeSeat(Vec<FeedSeat>)` schema from #3954). The pre-FeedSeat decoder misreads an allowlist length from mid-FeedSeat bytes and `deserialize_vec_with_capacity` preallocates (1,633,255,424 + 1) × 32 bytes. A contributor-rewards release cut from this pin decodes the account correctly. - No code changes needed — the workspace compiles unchanged against v0.31.0. Diff is `Cargo.toml` + `Cargo.lock` only, same shape as malbeclabs/doublezero-offchain#399. Follow-ups tracked separately (not in this PR): cap the unbounded `Vec::with_capacity` in the serviceability SDK helper, and make `serviceability::fetch` warn+skip undecodable accounts like `telemetry::fetch` does. ## Testing Verification - `cargo test -p doublezero-contributor-rewards`: 145 tests pass (100 unit + 45 integration), 0 failures. - Live decode replay against the testnet DZ ledger, replicating `serviceability::fetch` exactly (per-discriminator `getProgramAccounts`, Base64Zstd, finalized): all 1,956 serviceability accounts decode with 0 errors under an 8 GiB memory cap — including the EdgeSeat access pass above, which reproduces the byte-identical 52 GB allocation abort when fetched with the previous SDK pin.
malbeclabs/doublezero-offchain#404) ## Summary of Changes * `serviceability::fetch` no longer aborts the whole fetch mid-loop when one account fails to decode. The per-type account loop is extracted into `decode_accounts`, which warns (pubkey, account type, byte length, error), counts, and skips each account whose `try_from` returns `Err`, then returns the processed count and the failed pubkeys. * **Policy split (reward-bearing vs. reward-neutral):** an AccessPass decode failure is warned and skipped — AccessPass does not affect rewards. For every other type (Location, Exchange, Device, Link, User, MulticastGroup, Contributor), `fetch` completes the full sweep so every bad account gets a warn line, then returns `Err` naming the affected types, counts, and pubkeys. A partial snapshot on reward-bearing types would feed Shapley a silently shrunk graph (builders drop devices/links on missing cross-references), post a skewed merkle, and `rewards_exist_for_epoch` would freeze that epoch permanently. A loud failed epoch is recoverable; a frozen skewed merkle is not. * Each decode failure increments a Prometheus counter `doublezero_contributor_rewards_serviceability_decode_errors` (labeled by `account_type`), so even the skipped-AccessPass path can back an alert. The completion `info!` line still reports `Errors=` (per-type fetch failures) and `DecodeErrors=` (total decode failures) distinctly. * Fixes malbeclabs/doublezero-offchain#403 ## Scope / what this does and does not fix This is the ingest-policy layer only. It handles decode failures that surface as an `Err` from the SDK's `try_from`. It does **not** and cannot catch the 52 GB allocation abort from the 2026-07-22 incident — an allocation failure aborts the process before `try_from` can return, so it is not a catchable `Err`. That class is addressed by the SDK pin bump in malbeclabs/doublezero-offchain#402 (merged) and by allocation hardening in #4074. Given the pinned SDK's lenient `try_from` (per-field `unwrap_or_default`, `Err` only on a leading-discriminant mismatch) plus the offset-0 memcmp filter, most fetched accounts decode `Ok`; `Device::try_from` is the one processed type that can still `Err` mid-parse. The value here is a defined, loud policy for the decode failures that do surface, and parity of intent with `telemetry::fetch`. ## Testing Verification * One mixed-batch unit test on the extracted `decode_accounts`: a valid `Location` (built inline via `borsh::to_vec`) plus one undecodable account. Asserts the valid account landed in the locations map, exactly one decode failure is reported with the garbage pubkey, and the garbage account is not inserted. The test exercises the real skip-and-count loop (not a loop re-implemented in the test body), so a regression to `?` mid-loop would fail it. * `cargo test -p doublezero-contributor-rewards --lib` (101 passed), `cargo clippy --all-targets` clean, `cargo fmt` applied. ## Review history Earlier self-review rounds (design + security sub-agents) are recorded in the repo artifacts. This revision supersedes the initial blanket skip-and-warn policy with the reward-bearing/reward-neutral split above, per the plan owner's decision, and folds the per-type loop into the tested unit.
…oublezero-offchain#405) ## Problem `shreds pay`'s price floor read the **newest** metro/device ring entry, but `request_instant_seat_allocation` charges from the entries at the execution controller's **`last_settled_epoch`** — during `OpenForRequests` that is at least one epoch behind. While prices are static the two agree and nothing shows. The epoch a metro reprices, the preflight passes an underfunded amount straight through to an opaque `invalid account data for instruction`, which is precisely what the guard exists to prevent. Confirmed incident: testnet `lax-dz001` (`CT8mP6RUoRcAB67HjKV9am7SBTCpxaJEwfQrSjVLdZfD`) was repriced 43 -> 10; after the epoch rolled on 2026-07-24, `shreds price` quoted 10 while the program charged 43 prorated, failing three `qa.testnet` runs (`Payment escrow must have at least 41034841 / 39996869 / 34028528 USDC`). Mainnet has the same exposure — 15 devices across 7 metros dropped from 30/60 to 10. ## Changes - **`solana-sdk` `shred_subscription/state.rs`** — epoch lookup into the DeviceHistory/MetroHistory ring buffers (`parse_metro_history_price_at_epoch`, `parse_device_history_premium_at_epoch`), mirroring the onchain `RingBuffer::find`: walk backwards from `current_index`, bounded by `total_count`, so `epoch == 0` cannot match a zero-initialized slot. `seat_usdc_price_dollars` combines the base price with the signed premium the way `DeviceSubscription::usdc_price_dollars` does. Existing `parse_metro_history` / `parse_device_history` are untouched — the contributor-rewards ingestor and `shreds list` legitimately want the newest entry. - **`shreds pay`** — appends the execution controller to the existing batch fetch (no extra round trip) and computes the floor from `last_settled_epoch`. A missing ring entry now refuses to submit with a clear diagnostic instead of falling back to the newest entry; the program errors in that case too. The rejection message names both prices and both epochs: ``` Amount (10.000000 USDC) is below the seat price charged for this epoch (43.000000 USDC). The seat you are buying covers the remainder of epoch 946, priced at 43 USDC. The price changes to 10 USDC in epoch 947. ``` The per-seat price override keeps precedence, mirroring `checked_override_usdc_price_dollars().unwrap_or(default)`. - **`shreds price`** — new `Instant Price (USDC)` column / `instant_allocation_price` JSON field: what `pay` charges right now. `epoch_price` keeps its existing meaning (what a recurring subscriber pays next epoch). One extra key in the existing `try_join!` batch. The floor stays **unprorated**, as before. With `prorated_service_enabled` the program charges less, so the floor is stricter than strictly necessary — but it is always >= the program's requirement, so it can never cause the revert this fixes, and any surplus lands in the escrow. Proration would need `get_first_slot_in_epoch(current_subscription_epoch)`, which can be fast-forwarded past `clock.epoch + 1`; getting that subtly wrong would make the floor too low and reintroduce the revert. ## Verification - Unit tests for the ring search: exact hit, wraparound past index 0, the `total_count` bound, `epoch == 0` against both a zero-initialized and a written buffer, miss, truncated buffer. - Unit tests for the floor: the incident itself (settled 43, current 10, `--amount 10` rejected with the message above), signed premium applied at the settled epoch, override precedence, and no divergence line when the price is stable. - Live against DZ testnet, which still shows the divergence: ``` | lax-dz001 | xlax | Los Angeles | activated | 0 | 64 | 10 | 0 | 10 | 43 | ``` `epoch_price` 10, `instant_allocation_price` 43. Mainnet shows the same for the Mumbai devices (10 vs 30). Both converge once the epoch rolls. ## Follow-up (not in this PR) `malbeclabs/doublezero` `e2e/qa_shred_settlement_test.go` reads `epoch_price` from `shreds price --json` and pays it, so it will keep failing until it reads `instant_allocation_price` instead — but it now fails with the message above rather than a raw simulation dump.
…eclabs/doublezero-offchain#407) ## Summary of Changes Adds the fastshreds.com transition notice to `shreds pay` with a y/N gate so users see the notification before paying. Implements malbeclabs/infra#2164 (cross-repo, so it will need closing by hand). The notice block sits at `pay.rs:274-298`, entirely above `build_wallet` at line 301, so declining needs neither a loadable keypair nor a reachable cluster, and signs nothing. `command/mod.rs` is untouched — byte-identical to `main` — so the other callers of the shared prompt are unaffected by construction. How the gate behaves: 1. **Both stdin and stdout must be a terminal.** The prompt is written to stdout, so with stdout redirected the question would land in the file while `read_line` blocked on a terminal showing nothing. Anything short of a full terminal falls through to print-only, which is the behavior already settled for the non-interactive case. Dry runs also print without prompting, matching the epoch-remaining prompt, since a simulation signs and sends nothing. `--accept-deprecation-notice` skips the prompt for interactive batch and multi-seat workflows; the notice still prints. 2. **Declining bails, so it exits non-zero**, consistent with declining the epoch-remaining prompt in the same command, and so `shreds pay ... && notify` does not fire on a decline. The stronger reason for routing non-terminal stdin to print-only is that keypair-from-stdin (`solana-client-tools/src/keypair/loader.rs:49-52`) requires a non-terminal stdin, so prompting there would consume the keypair bytes. CHANGELOG.md updated. No metrics exposed. ### Copy Settled with the issue author, so nothing here is open. The `apt` upgrade instruction is dropped. The release ships deb, rpm and tar.gz (`release/.goreleaser.doublezero-solana-cli.yaml`) and `scripts/install-doublezero-solana.sh` auto-detects all three, so naming one package manager would have been wrong for rpm and tarball users. The abort message points at fastshreds.com instead, which is where an operator who declines is usually headed. The prompt keeps the shared helper's `Proceed? [y/N]` — a bare Enter counts as no — rather than the issue's `Would you like to continue? y/n`, for consistency with the epoch-remaining prompt in the same command. The notice's closing sentence was reworded to "This command will fund your seat now" so the appended question attaches to the payment rather than to closing the account. ### Out of scope, confirmed in review `shreds withdraw` carries no notice — the assumption is that it gets its own message once `shreds pay` is disabled. And raising the onchain `min_compatible_cli_version` in `malbeclabs/doublezero-shreds`, which is the only lever that reaches operators who never upgrade, should follow this shipping rather than precede it. ## Testing Verification **Mainnet-beta, end to end.** Declined at the prompt, dry-ran, then bought and withdrew a real seat in the same sitting with a throwaway keypair. Confirms what unit tests cannot: `y` carries through to a landed transaction, and the notice does not alter the payment path. Note this predates the dry-run change above, so the dry run in that session did prompt. **Terminal matrix**, with no keypair present and the RPC on a dead port. Verified across stdin/stdout as terminal, file and pipe, plus `--dry-run` and `--accept-deprecation-notice`: the notice prints in all eight combinations, and the prompt fires only for a full terminal with no skip flag. Invalid input re-prompts; `n` gives `Error: Aborted. Manage your account at https://www.fastshreds.com.` and exit 1; `y` proceeds into `build_wallet`. **Checks.** 451 workspace tests pass; `cargo fmt --check` clean workspace-wide. Local clippy cannot go green because `main` itself fails `-Dclippy::all` under rustc 1.95 (2 lints in `contributor-rewards`, 2 in `solana-cli`, none in files this diff touches, all confirmed present on unmodified `main`). The repo pins 1.92.0, so CI is the authority; `pay.rs` draws zero lints.
…albeclabs/doublezero-offchain#408) ## Summary - Add a `squads` module with Squads Protocol v4 vault support: the `SquadsArgs` and `OptionalSquadsArgs` clap argument structs, vault derivation and multisig verification, loader-v3 authority decoding, and base58 encoding of vault transactions for import into the Squads UI - Gate it behind a `squads` feature, on by default, so consumers can opt out with `default-features = false` - Take `Message` from `solana-message` and `UpgradeableLoaderState` from `solana-loader-v3-interface` rather than through `solana-sdk`, which deprecates the former re-export and does not carry the latter - Add a crate `README.md` covering the new module, with a placeholder for the rest of the crate ## Testing Eight unit tests cover the vault index rule across every `NetworkEnvironment`, the inspector URL encoding (including an endpoint that carries its own query string, where unescaped separators would otherwise read as parameters of the explorer link), the endpoint sharing warning, and loader-v3 authority decoding across the present, absent, and wrong-account-kind cases. Vault derivation is pinned by a known-answer test against a real devnet multisig and its vault, and the encoded payload is decoded back to assert the vault is fee payer and sole signer with a zeroed blockhash.
…quads vaults (malbeclabs/doublezero-offchain#409) Closes #4184 ## Summary - Add `vault_transaction_payload_budget`, reporting the bytes a payload of a given instruction count may serialize to, for a caller that grows a single instruction until it stops fitting - Add `try_encode_vault_transaction`, which measures the payload and refuses one that cannot work. `print_vault_transaction` becomes `try_print_vault_transaction` and routes through it - Derive the reserve as const arithmetic rather than in a comment, one term per line, from the `vault_transaction_create` envelope plus the compute budget pair, `proposal_create`, and `proposal_approve` the Squads app bundles into that same transaction. Compile-time assertions pin each block's subtotal and the 384 total, so a changed term fails the build instead of leaving a stale literal. A comment records the three app behaviors the derivation assumes away and what each costs - Refuse a payload naming a signer other than the vault, and one carrying more than 48 instructions. Both import and collect approvals before failing at execute, unlike an oversized payload, which fails at import - Check the `vault_transaction_execute` transaction as well, at 329 bytes plus 33 per payload account key. The issue specifies 277, which omits the compute budget pair the app bundles there too - Record the conventions this change was written under in `CLAUDE.md`, covering numeric literals, prose in comments and documentation, the `try_` prefix, and where workspace dependency features belong ## Testing - Hand-computed payload lengths at 3, 127 and 128 instruction-data bytes, spanning the legacy length prefix boundary - A payload landing exactly on the budget and one byte over it, at one instruction and at two - The wrapper Squads builds around a budget-sized payload, assembled and measured at 1,229 bytes of the 1,232 available - The widest payload the budget accepts, at 24 account keys and 1,121 bytes to execute - Refusals for an empty payload, a foreign signer, and 49 instructions - A downstream caller's vault print path against devnet, run before and after the change, byte-identical
…eclabs/doublezero-offchain#410) Closes #4183 ## Summary #### solana-sdk - Add `ValidatorClientRewards` as a `Pod` mirror of the onchain struct, with a `PrecomputedDiscriminator` impl, a `Default` impl matching the program's, a `checked_short_description` accessor, and a compile-time assertion pinning the account at 184 bytes - Delete the five `VCR_*_OFFSET` constants, `VCR_SHORT_DESCRIPTION_LEN`, `VCR_ACCOUNT_DATA_LEN`, the standalone discriminator constant, `parse_validator_client_rewards`, and `ValidatorClientRewardsInfo`. None of them shipped, so the changelog bullet that introduced them is rewritten rather than paired with a removal entry - Fold the new mirror into the existing mirrored-layout section alongside `ShredRewardToken` and `ValidatorPublisherRewards` rather than opening a second one #### solana-cli - Read the account through the mirror in `show`, `claim`, and `init-holding`. All three decode the whole struct, so they require at least the 184 bytes the program allocates - `claim` takes its post-transaction count from `SolanaConnection::try_fetch_zero_copy_data_with_commitment`, which warns on a missing or undecodable account where the previous code printed `(unavailable)` silently #### solana-fork - Build the synthetic account from `ValidatorClientRewards::default()` plus field assignment instead of copying bytes to hand-written offsets, and size the rent exemption from the built buffer. The parse-back round trip and its three `ensure!` checks go with them, being tautological against a struct the same function builds - Rename `--synthetic-vcr-manager` to `--synthetic-validator-client-rewards-manager`, along with the fork test script and the local-validator workflow #### repo - Record the conventions this change was written under in `CLAUDE.md`: the `_key` suffix for pubkey bindings, and which helper to reach for when reading a zero-copy account ## Testing - Unit tests across the three crates, including three cases for `checked_short_description` and an exact-match assertion pinning the rendered `show` summary byte for byte, which is what guards the format string's line continuations against a dropped escape - The `local-validator` workflow drives `show`, `init-holding`, and `claim` against a synthetic account baked into the fork at genesis, covering the renamed flag and the mirror's field offsets end to end
…wed (malbeclabs/doublezero-solana#123) ## Summary of Changes * `FinalizeDistributionRewards` now rejects a null rewards root whenever 2Z is owed to contributors — nonzero collected 2Z or an uncollected registered integration — not just when SOL debt is nonzero. * `CollectIntegrationRewards` now rejects an integration registered after the distribution's snapshot. * Why: finalize is permissionless and one-way. A null root latched while 2Z sits in the distribution strands that 2Z permanently; the old guard only covered SOL debt. Fixes malbeclabs/infra#1868. ## Testing Verification * New regression tests cover both guards. Full revenue-distribution SBF suite passes locally with the pinned Solana v3.0.12 toolchain under both feature sets; fmt and clippy are clean; refreshed sha256sums validated by reproducing the committed passport hashes.
…0ms 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.
…s org (malbeclabs/doublezero-offchain#412) Ahead of moving `doublezero-offchain` and `doublezero-solana` from the `doublezerofoundation` org to `malbeclabs`. ##⚠️ Do not merge before the transfer CI on this PR fails, and that is expected. `malbeclabs/doublezero-solana` does not exist yet, so cargo cannot resolve the new dep URL. The same reason makes the local pre-commit clippy hook fail, so the commit skipped it. Merge order: transfer both repos, then merge this, then re-run CI. ## What changed | File | Change | Why | |---|---|---| | `release/.goreleaser.*.yaml` (5) | `owner: doublezerofoundation` → `malbeclabs` | **The one real breakage.** GoReleaser writes the GitHub release to the owner named here. The new repo's `GITHUB_TOKEN` has no write access to the old org, so every release workflow fails until this lands. | | `Cargo.toml` | 3 `doublezero-solana` git deps | Tag `revenue-distribution/v0.3.7` is unchanged, so the same commit resolves from the new URL. | | `Cargo.lock` | 3 matching `source =` lines | URL swap only. The pinned sha `4368da2c` does not change, so no re-resolution. | | `Cargo.toml` | `repository`, `homepage` | Crate metadata. | | `scripts/install-doublezero-solana.sh` | `REPO=` and 4 curl examples | Public install path validators run. | | `CONTRIBUTING.md` | `doublezero-rewarder` → `doublezero-offchain` | That old name already redirects here. Left alone it would take two hops after the transfer. | ## Deliberately left alone - **`network-shapley-rs`** stays on `doublezerofoundation`. That repo is not moving and it is public, so the dep keeps working. Possible follow-up. - **CHANGELOG.md link footers** (~14 files). GitHub keeps redirects for old PR and release URLs, so these stay correct. ## Not a code change, still needed - Re-add the four repo Actions secrets if the transfer drops them: `CLOUDSMITH_TOKEN`, `GORELEASER_KEY`, `SLACK_BOTS_WEBHOOK`, `MALBECLABS_DOUBLEZERO_SSH`. The SSH one looks unused, every git dep is public. - Check the `main` ruleset and the `copilot` environment survived. - Cloudsmith is unaffected. Those repos (`doublezero`, `doublezero-testnet`, `doublezero-devnet`) live on Cloudsmith, not GitHub, and the apt package names do not change.
…clabs (malbeclabs/doublezero-solana#125) Ahead of moving `doublezero-solana` and `doublezero-offchain` from the `doublezerofoundation` org to `malbeclabs`. ##⚠️ Do not merge before the transfer `malbeclabs/doublezero-offchain` does not exist yet, so the `local-validator` workflow fails until both repos have moved. Merge order: transfer both repos, then merge this. ## What changed | File | Change | |---|---| | `.github/workflows/local-validator.yml` | `DOUBLEZERO_OFFCHAIN_GIT_INSTALL` — the `cargo install --git` URL for the offchain repo | | `programs/revenue-distribution/src/state/program_config/mod.rs` | RFC-0002 doc link | That is everything outside the changelogs. This repo is clean otherwise: no submodules, no crates.io publishing (`program-tools` is `publish = false`), no container images, and `verify-build.yml` only compares checksums, so no verified build is bound to the repo URL. ## Deliberately left alone **CHANGELOG.md link footers** (3 files, ~70 links to old PRs and tags). GitHub keeps redirects for those. ## Not a code change, still needed `.github/CODEOWNERS` names `@karl-dz` on every rule. A CODEOWNERS entry goes **silently dead** if the user lacks write access on the repo, with no warning. Add them to the `malbeclabs` org with write access on this repo before or with the transfer, otherwise review assignment stops working and nobody notices. Also worth checking after the move: the `main` ruleset and the `copilot` environment. This repo has no Actions secrets, so nothing to re-add. The wiki (enabled here) and the 5 forks travel with the repo.
…no leader schedule (malbeclabs/doublezero-offchain#413) ## Summary of Changes * The scheduler no longer writes a snapshot it cannot use: a failed leader-schedule fetch propagates instead of becoming `leader_schedule: null`, and `create_epoch_snapshot` validates before saving. * A failed fetch was one `warn!`, then an unusable snapshot uploaded over the epoch's canonical S3 key; the tick then failed reading it back with `Missing leader schedule`, a symptom whose cause survived only in the log. malbeclabs/doublezero-offchain#411 fixed this for the `snapshot` CLI command but not for the scheduler, which is the path that runs in production. * Scheduler failures now log the full cause chain (`{:#}`). Plain `{}` prints only the outermost context, so the newly propagated reason would still have been dropped. * **Security:** `EpochFinder`'s retry logs printed `reqwest`'s error verbatim, which includes the request URL. That URL carries the read endpoint's API key on mainnet-beta and journald ships to Loki, so those logs now strip it. This matters because the companion PR points mainnet-beta's reads at the keyed endpoint. * **Behavior change:** a `--dry-run` tick that previously marked the epoch processed with an unusable snapshot now fails and retries — nothing validated on that path, since `calculate_rewards` never ran. Both deployed environments run with dry-run off. ## Testing Verification * New `tests/test_snapshot_validate.rs` builds a `CompleteSnapshot` from the existing `testnet_snapshot.json` and `leader-schedule-epoch-89.json` fixtures: `validate()` is `Ok` on a complete snapshot, and names the right issue when the leader schedule is missing or empty.
There was a problem hiding this comment.
Pull request overview
This pull request imports the doublezero-offchain and doublezero-solana repositories into this monorepo under offchain/ and solana/ while keeping their histories and tags intact. It supports the monorepo migration plan by bringing both codebases in-tree without changing the existing workspace build behavior.
Changes:
- Add the
offchain/andsolana/source trees as nested workspaces. - Exclude
offchain/andsolana/from the root Cargo workspace so the root build remains unchanged. - Add a root
CHANGELOG.mdentry that documents the import.
Reviewed changes
Copilot reviewed 83 out of 424 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
Cargo.toml |
Excludes offchain/ and solana/ from the root workspace to avoid changing root builds. |
CHANGELOG.md |
Documents the repository import in the root changelog. |
offchain/… |
Imported offchain tools and services, including the scheduler and Rust crates. |
solana/… |
Imported Solana programs, mocks, and shared crates as a nested workspace. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| ```sh | ||
| mix deps.get | ||
| ## Installation | ||
|
|
||
| If [available in Hex](https://hex.pm/docs/publish), the package can be installed |
There was a problem hiding this comment.
This file is not authored by this pull request. It arrives byte-for-byte from doublezero-offchain, last changed there in #183 (2025-11-13). The unclosed code block is real, but editing it here would break this pull request's central gate, that git diff between each imported tree and its filtered source is empty. Worth a follow-up once the import lands.
| def handle_continue(:initialize_distribution, state) do | ||
| case nif_module().initialize_distribution(solana_rpc()) do | ||
| {:error, error} -> | ||
| Logger.error("initialize_distribution: received error: #{inspect(error)}") | ||
| {:stop, :shutdown, state} | ||
|
|
||
| {} -> | ||
| Logger.info("initialize_distribution: completed") | ||
| {:stop, :normal, state} | ||
| end | ||
| end |
There was a problem hiding this comment.
Not authored by this pull request. Arrives from doublezero-offchain #262 (2026-02-11) and is unchanged. The finding is real but the mechanism named is not: the worker reads Application.get_env(:scheduler, :solana_rpc), and it is offchain/scheduler/config/runtime.exs:6 that calls System.get_env("SOLANA_RPC") with no default, so an unset variable reaches the NIF as nil. System.fetch_env! at boot would fail loudly instead. Follow-up, not this pull request, which must keep each imported tree an exact copy of its source.
There was a problem hiding this comment.
Filed as malbeclabs/infra#2395. Two corrections while writing it up: a third worker, calculate_distribution.ex, has the same call, and DZ_LEDGER_RPC on the line above has the same gap but nothing in the application reads :ledger_rpc at all.
| [toolchain] | ||
| channel = "1.91" | ||
| components = ["rustfmt", "clippy"] | ||
| profile = "minimal" |
There was a problem hiding this comment.
Not authored by this pull request. Arrives from doublezero-solana #92 (2025-11-26). The mismatch is deliberate and documented: D3 of docs/superpowers/specs/2026-08-27-monorepo-migration-design.md keeps the Solana programs on 1.91 while the root workspace stays on 1.97.1, because the two target different chains and step 0 measured that 62 of the 96 crates in the closure resolve differently under a shared lockfile. The solana/ tree stays an excluded nested workspace with its own toolchain for exactly this reason.
| - Repo | ||
| - `doublezero-offchain` and `doublezero-solana` are imported into this repo, with their history and their release tags, under new top-level `offchain/` and `solana/` directories. Nothing that was already here moves. Both trees stay excluded from the root Cargo workspace and keep their own `Cargo.toml`, `Cargo.lock` and `rust-toolchain.toml`, so they build exactly as they did in their own repos, and their workflows do not run yet. The source repos are still the ones that release. (malbeclabs/infra#1952) |
There was a problem hiding this comment.
Fixed in 4e1ef8e, though not for the stated reason. The Unreleased section uses both forms: six cross-repo references (malbeclabs/infra#2114, #2387, #2343) against five same-repo ones (#4200, #4220, #4201), so a cross-repo reference is not out of convention here.
The real problem was which ticket it pointed at. It cited malbeclabs/infra#1952, the superseded prior-art issue, so a reader following the link landed on an abandoned plan instead of this work. It now cites #4240.
| def handle_continue(:collect_all_debt, state) do | ||
| case nif_module().collect_all_debt(solana_rpc()) do | ||
| {} -> | ||
| Logger.info("Successfully collected debts for all epochs") | ||
|
|
||
| {:error, error} -> | ||
| Logger.error("CollectAllDebt worker encountered an error: #{inspect(error)}") | ||
| end | ||
|
|
||
| {:stop, :normal, state} | ||
| end |
There was a problem hiding this comment.
Same finding as the one on initialize_distribution.ex, and the same answer: not authored here, arrives unchanged from doublezero-offchain #262 (2026-02-11). The single cause is offchain/scheduler/config/runtime.exs:6, not either worker. Follow-up.
| #[test] | ||
| fn test_lifetime_swept_2z_amount() { | ||
| let journal = Journal { |
There was a problem hiding this comment.
Not authored by this pull request. Arrives from doublezero-solana #115 (2026-04-21) and is unchanged. Renaming it here would break the gate that each imported tree is byte-identical to its filtered source. Worth a follow-up once the import lands.
There was a problem hiding this comment.
A few below that seem easier to solve before merging:
-
Merging is blocked by
required_linear_historyANDrequired_signatures, not just the linear history. Both need to be lifted to merge, as GitHub reports the 550 imported commits as unverified. -
Cargo.toml:25— Nothing in this repo builds or tests either imported tree, but Dependabot still edits them. Theseexcludeentries keepoffchain/andsolana/out of every root job, while.github/dependabot.yml's cargodirectories: "**/*"picks up their manifests: the next groupedrust-minor-patchPR — #4237 currently spans 5 directories, and they land weekly, not monthly — will carryoffchain/Cargo.lockandsolana/Cargo.lockto an all-green merge with nothing compiled, diverging the trees from the repos that still release them. Thegenerate-fixturesparallel does not hold:sdk-fixture-driftrunscargo runin all four of those.directorieshas no exclusion syntax, so the fix is enumerating the cargo directories. The same hole swallows a human edit to either tree. -
CHANGELOG.md:19— 451 of the imported commit subjects end in a bare(#N)that meant a PR in the source repo but resolves to an unrelated one here — 377 distinct numbers, and every sampled one exists in this repo. Once they land onmaineach renders as a link to the wrong pull request permanently, and GitHub posts cross-reference backlinks on those threads. Only fixable before the merge:git filter-repo --message-callbackcan rewrite them to(malbeclabs/doublezero-offchain#N)in the same pass that relocates the tree. -
CHANGELOG.md:19— Nine tags do not come across. The sources hold 126 but only 117 are reachable from theirmain, socontributor-rewards/v0.2.2,offchain-scheduler/v0.1.0,doublezero-solana/v0.0.1and six rc tags are dropped — six of the nine have published GitHub Releases. Same orphan class as thesentinel/v0.6.1handled under D5, but the entry reads as if all tags survive.
Relocated with git-filter-repo --to-subdirectory-filter offchain, so every path from the source repo lands under offchain/ and nothing existing moves. All 110 tags come across unprefixed, which keeps each component's version line continuous. Commit messages are rewritten in the same pass so that a bare (#N), which meant a pull request in the source repo, becomes malbeclabs/doublezero-offchain#N. Left alone, 451 of them would resolve to unrelated pull requests in this repo and post cross-reference backlinks on those threads. Step 2 of the monorepo migration.
Relocated with git-filter-repo --to-subdirectory-filter solana. The tree keeps its own workspace, lockfile and rust-toolchain.toml, so it builds exactly as it does today. Commit messages are rewritten so a bare (#N) becomes malbeclabs/doublezero-solana#N. Step 2 of the monorepo migration.
Both trees keep their own Cargo.toml, Cargo.lock and rust-toolchain.toml and build as nested workspaces, exactly as they did in their own repos. Step 4 of the monorepo migration folds offchain into the root workspace. The solana tree stays excluded because 62 of its 96 closure crates resolve differently under a shared lockfile. The cargo entry in dependabot.yml globbed "**/*", which would have found both imported manifests. No job here builds either tree, so a grouped update would have edited their lockfiles, merged green with nothing compiled, and diverged them from the repos that still release them. The five directories listed are every one that has its own lockfile, which is what the glob resolved to before the import.
4e1ef8e to
8bb4791
Compare
Step 2 of the monorepo migration, per
docs/superpowers/specs/2026-08-27-monorepo-migration-design.md. Importsmalbeclabs/doublezero-offchainandmalbeclabs/doublezero-solanainto this repo with their history and their release tags.Summary
git filter-repo --to-subdirectory-filterand merged with--allow-unrelated-histories, so every path from each source repo lands under a new top-leveloffchain/orsolana/and nothing that was already here moves. Zero conflicts in either merge.contributor-rewardsrelease picks up fromv0.6.1rather than restarting.filter-repopass. A bare(#N)in a source repo meant a pull request there; here it would resolve to an unrelated one. 451 subjects carried one, across 377 distinct numbers, and every number sampled exists in this repo. They now readmalbeclabs/doublezero-offchain#Nandmalbeclabs/doublezero-solana#N. References that were already qualified are untouched.offchainandsolanajoin the root workspaceexcludelist. Both keep their ownCargo.toml,Cargo.lockandrust-toolchain.tomland build as nested workspaces, exactly as they did in their own repos. The rootCargo.lockdoes not change..github/dependabot.ymlenumerates its cargo directories instead of globbing**/*, which would otherwise have found both imported manifests. Nothing here builds either tree, so a grouped update would have edited their lockfiles, merged green with nothing compiled, and diverged them from the repos that still release them. The five listed directories are every one with its own lockfile, which is what the glob resolved to before the import..github/workflows/are not promoted to the repo root, so none of the eleven workflows runs. GitHub Actions reads workflows only from the root, so the nested copies are inert, and leaving them in place keeps each imported tree an exact copy of its source and gives step 5 its source material in-tree.The orphan
sentinel/v0.6.1tag was deleted first, per D5. It had no release and no goreleaser config behind it, and it sat above offchain's live sentinel line, which reachesv0.2.6. The collision check was re-run immediately before the import and still prints exactlysentinel;doublezero-solanacollides with nothing.Merge instructions
This must be merged with "Create a merge commit", not squash. A squash collapses 550 commits into one and destroys the history this pull request exists to preserve.
Two rules on
mainblock that and both need lifting for the merge, then restoring:required_linear_history, which refuses merge commits outright.required_signatures.git filter-reporewrites every tree, which invalidates the signatures the source commits carried, so all 550 arrive unsigned. Nothing can re-sign another author's commits.The
pull_requestrule is separate and stays on throughout, so nobody can push tomaindirectly during the window.The tags are pushed after this merges, so they point at commits that are ancestors of
main.Size
424 files and roughly 589,000 insertions, far above this repo's 500-line guideline. It cannot be broken up: an import of two repositories is one operation, and splitting it would leave
mainholding a half-imported tree. Note that 82% of the insertions are five JSON files, two of which arecontributor-rewardstest snapshots totalling 483,000 lines. The Rust is 74,000 lines across 292 files, and all of it is unchanged from the source repos. The reviewable surface of this pull request is the three files outside the imported trees; the checks below establish that everything else is a copy.Testing Verification
git diffbetween each imported tree and its filtered source is empty. Both trees are byte-identical to whatgit filter-repoproduced from the sourcemain.offchain/andsolana/, exactly three files change:Cargo.toml,CHANGELOG.mdand.github/dependabot.yml. No existing job definition is touched.#Nreference remains anywhere in the imported history, in subjects or bodies.Cargo.lockis unchanged, and the root workspace resolves with the two new directories excluded.go build ./...scope is unaffected.v*.*.*tag, so pushing the tags fires no release.Follow-ups, deliberately not in this pull request
SOLANA_RPCboots the scheduler clean and fails later inside a worker, andDZ_LEDGER_RPCis read into config that nothing consumes..dockerignoredoes not exclude the new trees, so therelease.docker.core.ymlbuild context grows by about 31 MB. That workflow fires only on barev*.*.*tags. Step 5 merges the release setup and is the place to decide whether the root images need either tree.