All notable changes to this project will be documented in this file.
- CI
- The Agave toolchain install retries, and a failed one now fails the job. Eight workflow steps across five workflows ran
sh -c "$(curl -sSfL .../install)"once with no retry, so a transient reset fromrelease.anza.xyzkilled a job before it ran anything. That form also swallowed a failed fetch: the command substitution comes back empty,sh -c ""exits 0, and the step passed having installed nothing. The eight are now one composite action at.github/actions/solana-toolchainthat fetches and runs as separate steps, checkssolana --versionactually runs, clears partial state between attempts, bounds every wait so a stalled handshake or hung transfer reaches the backoff instead of sitting until the job times out, and backs off. Each caller keeps the version it used before;solana.ymlandoffchain.local-validator.ymlare on v3.0.12 and the other three on v3.0.4, which is drift worth settling separately.
- The Agave toolchain install retries, and a failed one now fails the job. Eight workflow steps across five workflows ran
- Serviceability
write_stake_mirrorin the instruction crate andWriteStakeMirrorCommandin the Rust SDK, so the relayer has a caller-side surface for the instruction A6 added. The command departs from its neighbours in one way: every other one usesappend_payer_permission_account, which attaches the caller'sPermissionaccount only when it already exists, because a legacyGlobalStatekey might authorize instead. No legacy key satisfiesSTAKE_ORACLE, so a missingPermissionaccount is always fatal here, and the command says so locally rather than sending a transaction that can only come backNotAllowed. It checks whatauthorizechecks, not just ownership: an account that does not decode, a suspendedPermission, and one lacking the flag are each refused with the reason named, which matters because suspending aPermissionis what revoking the relayer's key looks like.- New
WriteStakeMirrorinstruction (variant 119) and aSTAKE_ORACLEpermission, which is what a relayer will call to copy a builder's Solana stake onto the DZ ledger. Nothing could write aStakeMirrorbefore this. The instruction refuses a write whosesource_slotis not newer than the stored one: polling makes a repeated write harmless but says nothing about ordering, and a retry carrying an older read could otherwise walk the mirror backwards, so the program enforces it rather than trusting the caller. It also refuses to reassign a mirror to a different builder, since the builder is not a PDA seed, and it carriesfeed_keyforward rather than taking it from the caller, becauseCreateFeedwrites that to spend the stake and zeroing it would let one bond back two feeds. No legacy GlobalState key maps toSTAKE_ORACLE, so a holder needs a realPermissionaccount even whilerequire-permission-accountsis clear. Gated onallow-staked-feedswith the rest of RFC-28.STAKE_ORACLEis grantable throughdoublezero permission set --add stake-oracle, named bypermission auditandbitmask_to_names, listed inAUTHORIZE_GATED_FLAGSand in the Go SDK's flag constants. It is the first flag no legacyGlobalStatekey can satisfy, which the audit's own test now asserts through aPERMISSION_ONLY_FLAGSlist rather than treating as a gap in the enumeration. Feedcarries the RFC-28 stake terms:builder,stake_ref,spec_id,sla_hash,committed_rate_bits_per_secand a lifecyclestatus. Setting them needs the newallow-staked-feedsfeature flag, which no cluster has, soCreateFeedrefuses a builder until the stake mirror and the attestor exist; without the flag the instruction behaves as before. The fields are appended rather than versioned, so a feed written before this decodes with them defaulted andtry_acc_writeresizes the account on the next update.statusis the exception to defaulting: a short account readsActive, because reading it asPendingwould pull every live catalog feed out of service. The rate is bits per second, not basis points, which is whatbpsmeans elsewhere in DoubleZero.- New
StakeMirroraccount (AccountType19), one per stake, holding the stake a relayer saw on Solana: tier, the rate the builder declared, the source slot, and the key that vouched for it. The PDA is seeded bystake_ref, thebuilder-stakeaccount it mirrors, not by the builder: RFC-28 collateralizes each feed on its own bond, so a builder-keyed mirror would let two feeds pass the coverage check against one stake. The DZ ledger cannot read a Solana account, so this is an assertion rather than a proof, andrelayerrecords whose assertion it is.StakeTier::Noneis the default and covers no rate, so an unwritten mirror and an absent one are the same answer to a reader. Nothing writes one yet; who is allowed to is the open trust-anchor question. CreateFeedrefuses a staked feed whose committed rate the stake tier does not cover, reading the tier from the stake'sStakeMirror, and refuses a stake that already backs a feed. Creating a feed claims the stake by writing the feed's key onto the mirror, which is what makes RFC-28's one feed per stake true; a relayer updating a mirror has to carryfeed_keyforward rather than zero it. The mirror account is found by matching its PDA rather than by position, so a caller that sends one is not obliged to also send aPermissionaccount and a caller that sends neither is unaffected. Four errors rather than one generic argument failure:StakeMirrorMissing(119) when the account is left out,StakeDoesNotCoverRate(120) when the stake is absent or too small,StakeAlreadyBacksFeed(121) when it is already spent, andStakedFeedCannotBeDeleted(122) becauseDeleteFeednow refuses a staked feed: closing it would leave the claim pointing at an account that no longer exists, so the bond would back nothing and still refuse to back anything else. Retirement is the path out and releases the stake with it, both in the lifecycle work.- A feed admits a subscriber only while its status is
Active.SubscribeFeedandCreateSubscribeUserboth check it where the seat is spent, so a feed that is pending conformance, halted, or retired stops taking subscribers the moment its status changes, and retiring one needs no sweep over the access passes already holding a seat for it. The check is deliberately not in the shared coverage path, whichUnsubscribeFeedalso runs: gating there would leave a user holding a seat on a retired feed with no way to release it. New errorFeedNotActive(123). Live subscribers are unaffected until they disconnect; evicting them is separate work. - Test coverage for the RFC-28 publish-rights path, which needs no new instruction. A feed's multicast groups are created with
ownerset to the builder, andAddMulticastGroupPubAllowlistauthorizes onmgroup.owner == payer, so the builder grants its own publish rights without the catalog admin that created the feed. The test walks it with two distinct signers.
- SDK
- The Rust SDK sends program logs and errors through the log facade instead of straight to stderr. A failed transaction's program logs now arrive as one
error!record carrying the signature, so a service embedding the SDK can capture, filter and ship them. Foureprintln!sites bypassed logging entirely, while the same function logged the successful path at debug level: an operator saw every transaction that worked and got raw stderr for one that failed. RFC-20 already asks modules to use the standard log macros. One behavior change for the CLI:--log-level offsilences program logs, which previously printed whatever the level (#4299). sdk/shreds/gocarries the feed subscription program'sFeedDistributionaccount: how much USDC one feed collected for one calendar month. The program is a second program alongside shred subscription and had nothing in this SDK, so each consumer decoded the account at fixed byte offsets itself, lake included. The account is a bytemuck Pod read here field by field, which agrees with the Pod bytes only because the field order leaves no interior padding;TestStructSizespins the 120-byte total and a new test pins every field against a real mainnet account.Clientis built around one program ID and so gains no fetch method, andDeserializeFeedDistributionis exported for a caller that makes its owngetProgramAccountscall.make sdk-testnever ran./sdk/shreds/go/..., so this package's layout pins have never run in CI; it runs them now. (#4216)- The TypeScript and Python
Feeddeserializers read the RFC-28 tail and synthesizeActivefor an account that carries no status byte, matching the Rust program. Newfeed_legacyfixture covers that path alongside the updatedfeedfixture.
- The Rust SDK sends program logs and errors through the log facade instead of straight to stderr. A failed transaction's program logs now arrive as one
- Solana programs (
solana/)builder-stakecarries its own instruction builders ininstruction::builders, moved out of the test harness now that a caller outside the crate needs them. Each one fixes the account order the processor expects, so a change there has one place to update rather than one per caller.builder-stakeexposes itsprocessormodule andtry_process_instructionunder the existingentrypointfeature, so a test in another crate can load the program natively throughprocessor!rather than building it to BPF first.doublezero-serviceabilityalready exposes its own for the same reason. The crate's own tests still run against the.so, and nothing is exposed to a consumer that does not ask for the feature.builder-stakeholds a bond for six months and returns the excess after. The first bond starts the hold and a later one does not restart it, so a repricing that forces a top-up cannot push a builder's withdrawal date out.Withdrawreturns anything above the stake's requirement to a token account the builder names, and refuses both before the hold elapses and below the requirement, which is what stops a builder walking its bond out from under a live feed. The hold is 180 days rather than calendar months, because a month has no fixed length and the alternative is calendar arithmetic onchain to move a one-off boundary by at most three days. A stake that has never been bonded has no hold, and a zero expiry means the hold has not started rather than that it ended in 1970. Every instruction taking a stake checks the account is at the address its own fields derive, which the zero-copy reader does not do. ASetHoldExpiryinstruction lets a devnet demo show a withdrawal without waiting: it exists in every build and refuses outside adevelopmentone, rather than sitting behind a#[cfg]that would give the two binaries different instruction encodings for the same bytes.- New
builder-stakeprogram atdzbschFChpPoWihZFdnYjyzHJicZwPHb6QTntHjhLki, holding the 2Z bond a builder posts before deploying a feed under RFC-28. ABuilderStakePDA, a 2Z token account owned by it, andInitializeProgram,SetAdmin,ConfigureProgram,InitializeBuilderStakeandPostBond. A bond rather than a deposit: it is returnable after the hold and forfeitable by slashing, anddepositcarries neither. The address is keyed on(builder, stake_index)rather than the builder alone, because RFC-28 collateralizes each feed on its own bond and a builder-only address would cap a builder at one stake for life. The program starts paused, so a deployment with no admin and no tier table holds nothing. No slash instruction yet: the burn authority is what makes this its own deployable, and writing it before the verdict signer is settled means writing it twice. Bond sizing and the six-month hold are not here either. builder-stakesizes a bond against the rate its feed commits to. An admin sets three 2Z amounts, one per RFC-28 rate tier; RFC-28 quotes the tiers in dollars but fixes the bond in 2Z at the price prevailing when the tier is set, so this is a table rather than a price feed. The rate ceilings are compiled in, because they have to agree withStakeTierin the serviceability program. A stake's requirement follows the tier table while the stake is short and stops moving once it is funded, so repricing cannot under-fund a builder who already paid in full, and cannot be dodged by pre-creating stakes for the cost of rent and funding them after a rise. A table with a hole, or one where more rate costs less, is refused.
v0.39.0 - 2026-09-04
- CLI
- Remove
revenue-distribution convert-2zandharvest-2z(malbeclabs/infra#2527). - Validator deposit no longer accepts
--convert-2z-limit-price.
- Remove
- SDK
- Remove the Go, Python, and TypeScript revenue-distribution clients that fetched
/swap-rate.
- Remove the Go, Python, and TypeScript revenue-distribution clients that fetched
- Monitor
- Stop polling the SOL/2Z swap oracle (malbeclabs/infra#2527).
-twoz-oracle-intervalstill parses so existing ansible extra args do not fail the process.
- CLI
revenue-distribution fetch sol-conversionno longer requests a swap quote.
- SDK
- The TypeScript and Python
GlobalStatedeserializers exposeip_verifier_authority_pk, the RFC-27 trust root the Go SDK and the Rust state already carried, so those consumers can read which key signs IP ownership proofs. The field is appended, so an account written before the upgrade decodes it as the default pubkey rather than failing. (#4231)
- The TypeScript and Python
- CI
- The e2e matrix runs 5 round-robin shards instead of 4. Shards are filled by test count, not by duration, so the heaviest one was carrying ~3735s of tests against a 15-minute job timeout and was cancelled mid-run; the extra shard brings the worst case back to ~3022s. Adding
e2e (shard 6)to the required status checks in the main ruleset is a separate, manual step. - shreds-e2e pins one heavy test to its own shard instead of three.
TestE2E_MultiUserInstantAllocationAndWithdrawalandTestE2E_DeviceScaleno longer exist in doublezero-shreds, so the pin validation failed every run and the matrix was never built. OnlyTestE2E_FeedSubscriptionOracleExpiryTeardownstays pinned, leaving 1 pinned + 2 round-robin shards. Droppingshard-e2e (shard 4)andshard-e2e (shard 5)from the required status checks in the main ruleset is a separate, manual step — until it happens those contexts are required but never reported. .cursor/BUGBOT.mdand.github/copilot-instructions.mdnow tell Bugbot and Copilot to read the nearest sibling, flag a path that skips a zero or a duplicate, and assert a specific error and the exact log line at the expected index. Onchain checks apply only when the repository has onchain code. The eight path-scoped files under.github/instructions/are removed so Copilot reads only the repo-wide file. (#4247)- The e2e shard step passes
GITHUB_TOKENtogo test.TestE2E_BackwardCompatibilityasks the GitHub API which client releases exist, and unauthenticated that is 60 requests an hour shared across every runner on the same address, so shard 1 failed on a 403 rate limit before reaching a devnet. The test already read the variable; nothing was setting it. - The 11 workflows imported with
doublezero-offchainanddoublezero-solanaare folded into this repo's set, and the imported.githubdirectories are deleted. GitHub only reads workflows at the repository root, so those files did nothing where they sat. - Five release workflows come across, one per component: contributor-rewards, doublezero-solana-cli, the offchain sentinel, solana-validator-debt and the offchain scheduler. They run goreleaser from the repository root rather than from
offchain/, because the merged workspace writes to the roottarget/directory, which is where goreleaser's rust builder looks for the binary it packages. Every path inside the five goreleaser configs is rewritten to match, andrelease.github.namemoves fromdoublezero-offchaintodoublezero, so the releases land on this repository. The three secrets they need are already configured here. - New
solanaworkflow. Thesolana/tree is excluded from the root workspace, sorust.ymlnever reaches it and it would otherwise have no CI here at all. It runs that tree's own lint, library tests, docs and SBF tests, plus the checksum gate that rebuilds both networks' artifacts and verifies them againstsolana/programs/sha256sums_*.txt, path-scoped tosolana/**. - New
elixirworkflow for the offchain scheduler, path-scoped tooffchain/scheduler/**: format check, compile with warnings as errors, credo and tests. Those checks only ran on a release tag before, so a pull request that broke the scheduler passed. - New
offchain-local-validatorworkflow carrying the two live fork tests. It runs from the repository root, since the shell scripts resolve their binaries astarget/debug/<name>. Every cargo call names its package, because the root workspace setsdefault-members = []and a barecargo build --binselects nothing. It is path-scoped to the trees that can affect it rather than running on every pull request as it did before. The two jobs that were disabled or commented out upstream are not carried over. rust-cli-staticnow asserts static musl linkage for all five released CLIs instead of the client alone, which is what offchain'srust-musl-staticjob did for its four. Each package is built on its own so feature unification matches the release.changelog-reminderkeeps the per-crate changelog check that offchain enforced, with paths moved underoffchain/. A change to one of those 13 subprojects needs both the rootCHANGELOG.mdand that subproject's own.- Offchain's
ci.ymlis dropped:make rust-build,make rust-lintandmake rust-testcover those crates now that they are workspace members. Itsjust cicoverage floor (cargo llvm-cov --fail-under-lines 25) is not carried over, since the workspace it measured no longer exists.offchain/Justfilekeeps its Elixir recipes and loses the Rust ones, which would otherwise act on the whole workspace with a different fmt and clippy line than CI uses. offchain/scripts/release-rc.shresolves the repository root two levels up rather than one, so local release candidates build against the merged workspace.- The three
contributor-rewardstests that set and removeREWARDER_KEYPAIR_PATHare marked#[serial]. That variable is process-wide, andcargo testruns tests as threads in one process, so they raced: one test removing it while another was between setting it and reading it made the read fail. It never fired indoublezero-offchain, whose CI rancargo nextest, which gives each test its own process.
- The e2e matrix runs 5 round-robin shards instead of 4. Shards are filled by test count, not by duration, so the heaviest one was carrying ~3735s of tests against a 15-minute job timeout and was cancelled mid-run; the extra shard brings the worst case back to ~3022s. Adding
- E2E/QA
- New e2e coverage for RFC-27 proof enforcement with
require-ip-ownership-proofset: the working path still reaches BGP, a client with no verifier to reach is rejected withIpOwnershipProofRequired, a wildcard (0.0.0.0) access pass bindsclient_iponly when a proof is attached, the sentinel authority stays exempt so the oracle path keeps working, andconnectrefuses a proof whose address disagrees with the one it provisions. (#4243) - Remove
TestQA_MulticastSettlement. It funded a seat throughdoublezero-solana shreds pay, which is going away. The agent seat-pay RPC now returns Unimplemented if something still calls it. Unused settlement helpers go with the test. (#4248)
- New e2e coverage for RFC-27 proof enforcement with
- Offchain
- The scheduler refuses to boot when
SOLANA_RPCis unset or empty, rather than handingnilor an empty string to the Rust NIF. All three workers read the one config key, so the check sits inconfig/runtime.exswhere the single cause was. The test environment is exempt, since the tests set the key themselves.DZ_LEDGER_RPCno longer sets aledger_rpckey that nothing in the application read. (#4240 follow-up) - The scheduler README closes its shell code block, so the Installation heading and everything after it stop rendering inside it.
- The scheduler refuses to boot when
- Solana programs
- Rename
test_lifetime_swept_2z_amountto match thelifetime_swapped_2z_amountfield and method it covers, so a search for the swapped-amount tests finds it.
- Rename
- Repo
- The offchain crates join the root Cargo workspace.
offchain/Cargo.toml,offchain/Cargo.lockandoffchain/rust-toolchain.tomlare gone, so all 15 crates build on this repo's 1.97.1 toolchain against one lockfile, and the ten path dependencies resolve inside a single workspace. Thesolana/tree stays excluded with its own lockfile and its 1.91 toolchain: 62 of the 96 crates in the programs' build closure resolve differently here, which would change the compiled bytes and breaksolana/programs/sha256sums_*.txt. (#4240) - Each offchain crate now states
edition = "2024"and its own version. Both were inherited from the offchain workspace, so folding them in would have moved all 15 crates to edition 2021, where they do not build, and reversioned four of them from 0.0.1 to 0.38.0. bincodeandreqwestare declared per crate in the offchain tree rather than taken from the workspace. This repo is on bincode 2, whose API differs, and on reqwest with its default TLS backend, while the offchain crates use bincode 1 and rustls with the OS root store. Unifying either is a code change, not a manifest change.- The e2e sentinel binary is renamed from
doublezero-sentineltodz-e2e-sentinel. Offchain ships a deb of the former name, and two members of one workspace cannot write the same file intotarget/release. The e2e base image and the sentinel image follow the new name. - The e2e base image builds its four binaries by name instead of
--workspace, so it no longer compiles the offchain crates and their arrow, parquet and AWS dependencies on the way to a client and a sentinel. - The revdist fixture generator reaches
doublezero-program-toolsanddoublezero-revenue-distributionby path intosolana/instead of by an unpinned git dependency, matching the other three generators. Nothing there floats on the nextcargo updateany more. - Offchain's release profile (
lto = true,codegen-units = 1) is not carried over. Cargo profiles are workspace-wide, so keeping it would apply full link-time optimization to every release build in this repo, including the ones e2e waits on. The offchain release binaries lose that optimization, which is accepted rather than deferred. smartcontract'stest-sbftarget runs from each program's directory, the waybuild-programsalready does, instead of once fromsmartcontract/. An unscopedcargo test-sbfresolves the whole workspace against the platform-tools rustc, which reports itself as 1.89.0-dev, and the workspace now carries crates declaring a higher minimum:aws-sdk-s3at 1.94.1 andrustlerat 1.91, both reached through the offchain crates and neither in any program's dependency graph. Serviceability runs before telemetry, whose test helper loads the serviceability.sofromtarget/deploy.smartcontract'stest-sbftarget names its four program packages instead of running unscoped. An unscopedcargo test-sbfresolves the whole workspace against the platform-tools rustc (1.89.0-dev), and the workspace now carries crates declaring a higher minimum,aws-sdk-s3at 1.94.1 andrustlerat 1.91, both reached through the offchain crates. Neither is in any program's dependency graph. The siblingtest-programsandlint-programstargets were already package-scoped, which is why only this one failed.- The imported
offchain/tree catches up with the three pull requests that landed indoublezero-offchainafter the import point:doublezero-solana shreds payis removed (withdraw,list,paymentsandpricestay), the fund-seat instruction is removed from the offchain Solana SDK whileshreds paymentskeeps reading leftover fund-seat transactions, and the solana-cli crate moves to 0.5.12. Author, date and message come across unchanged, with the pull request references named the way the import named its own.solana/needed no sync: its upstream tip is the imported tip. offchain/CONTRIBUTING.mdis removed and its code of conduct moves to the root README. It told contributors to forkdoublezero-offchainand open pull requests and issues there, which stops being true when that repository is archived, and the rest of it duplicated the root's contributing guidance.- DEVELOPMENT gains a section on the two imported trees, naming the parts the root
maketargets do not cover: the Solana L1 programs, which keep their own workspace and toolchain, and the Elixir scheduler.
- The offchain crates join the root Cargo workspace.
v0.38.0 - 2026-08-28
- SDK
UpdateMulticastGroupRolesCommand.group_pk: Pubkeybecomesgroup_pks: Vec<Pubkey>andCreateSubscribeUserCommand.mgroup_pk: Pubkeybecomesmgroup_pks: Vec<Pubkey>(non-empty; the first entry is the instruction's primary group). The RFC-26 buildersupdate_multicast_group_rolesandcreate_subscribe_usergain anextra_groups: &[Pubkey]parameter and derive the newextra_group_countarg from it. Single-group callers pass a one-element vec / empty slice.CreateSubscribeUserCommandmeasures the built transaction's wire size and rejects a group set that cannot fit under the 1232-byte limit, naming how many groups do fit: the create also carries the device's dz_prefix accounts and an optional feed, so the 16-group role-update chunk does not bound it. (malbeclabs/infra#2114)
- CI
- The new
release-bump-dry-runjob dry-runs the release version bump on every PR, so acargo update --workspacedependency rebind fails on the PR that causes it instead of a week later at release time, as it did for v0.37.0 (#4213, fixed in #4219). Advisory until its context is added to themainruleset. (#4220) - Add
.cursor/BUGBOT.mdto enable Cursor review. This is an experiment. (malbeclabs/infra#2387)
- The new
- Repo
- The ten git dependencies that
offchain/carried onmalbeclabs/doublezeroandmalbeclabs/doublezero-solanabecome path dependencies into this repo. All ten version pins stop existing, so the two trees can no longer disagree about which revision of a shared crate they build, and a change spanning them lands in one commit instead of a pin bump per consumer.network-shapley-rsstays a git dependency, since it lives in another organization. (#4245)
- The ten git dependencies that
- Repo
doublezero-offchainanddoublezero-solanaare imported into this repo, with their history and all 126 of their release tags, under new top-leveloffchain/andsolana/directories. Nothing that was already here moves. Both trees stay excluded from the root Cargo workspace and keep their ownCargo.toml,Cargo.lockandrust-toolchain.toml, so they build exactly as they did in their own repos, and their workflows do not run yet. Imported commit messages are rewritten so that a reference that meant a pull request in a source repo now names that repo explicitly..github/dependabot.ymlenumerates its cargo directories instead of globbing them, so no update lands in a tree this repo does not build. The source repos are still the ones that release. (#4240)
- CLI
doublezero connectwith no mode provisions everything the server's AccessPass authorizes in one run — the IBRL tunnel plus a multicast tunnel joined to the groups or purchased feeds the pass grants — instead of requiring the operator to know their entitlements and issueconnect ibrlandconnect multicastseparately. Each mode is reported on its own line; one the pass does not cover is skipped with the reason rather than failing the run. The two are attempted independently, so a failure in one keeps the other's work and names the command that finishes the rest, and the run exits non-zero if an attempted mode failed or the pass authorized nothing. Epoch expiry still gates unicast only, so an expired pass connects multicast and skips IBRL. The bare form also enables the reconciler up front, so a run that provisions nothing still leaves the daemon managing tunnels, and takes--tenant/--allocate-addrfor its IBRL half (connect ibrlkeeps its own positional tenant and-a).connect ibrlandconnect multicastare unchanged.doublezero balancetakes an optional address, sodoublezero balance <pubkey>reports that account's balance while the bare form keeps reporting the configured keypair's. Querying another account needs no local keypair. An address that was never funded prints0 Creditsinstead of failing the account lookup.- New
doublezero transfer <RECIPIENT> <AMOUNT>sends credits from the configured keypair to another account on the DoubleZero Ledger, mirroringsolana transfer:AMOUNTis a credit amount, orALLto send the whole balance minus the transaction fee. A recipient that does not exist yet is created by the transfer, with the amount raised to the rent-exempt minimum when it falls below it, so no opt-in flag is needed for an unfunded recipient. A transfer that would leave the sender holding a nonzero balance below that same minimum is refused up front, with the largest payable amount named, since the runtime would reject it. doublezero connect Multicastwith N groups is one transaction in the common case (all groups sharing one publisher/subscriber flag pair fold into the create, skipping the activation wait);doublezero multicast subscribe|unsubscribe|publish|unpublish,doublezero user subscribe, and the role-strip cleanup inuser delete/request-banbatch their role changes by flag pair, chunked to 16 groups per transaction. Failure reporting in the multicast verbs is per batch: a failed batch lists every group it carried, since none was applied. (malbeclabs/infra#2114)doublezero feed update|delete --force-unsubscribestrips each user's orphaned groups with one batched role update per user (chunked to 16 groups per transaction) instead of one transaction per group. (malbeclabs/infra#2114)doublezero connectobtains an RFC-27 IP ownership proof from the verification service and attaches it to user creation; the address the service observes is authoritative, and where it disagrees with the daemon's own discoveryconnectstops and names both. A verifier that is unreachable, unconfigured, or that declines is reported and the connect continues without a proof, which the program accepts untilrequire-ip-ownership-proofis set;--ip-verifier-urlorDZ_IP_VERIFIER_URLpoints at one, and no environment has a built-in default yet. (#4201)
- Client
- From-source builds per
client/INSTALL.mdnow succeed.client/MakefiledefaultedCARGO_FLAGSto empty, somake buildproducedtarget/debug/doublezerowhilemake installcopied fromtarget/release/doublezero, which never existed;CARGO_FLAGSnow defaults to--releaseso the two agree.make installalso called Debian-onlyaddgroup/adduser, which are absent on RHEL/Rocky/Amazon Linux (in the documented support matrix), failing withaddgroup: command not found; it now uses the portablegroupadd/useraddwith equivalent flags. (#4175)
- From-source builds per
- Serviceability
UpdateMulticastGroupRoles(58) andCreateSubscribeUser(59) accept additional writable MulticastGroup accounts (counted by a new borsh-incrementalextra_group_count: u8arg), so subscribing a user to N groups is one atomic transaction instead of N: one signature/fee, and a failure rolls back every group. Each batch member is authorized exactly like a single-group call (per-group allowlist checks; inCreateSubscribeUser, EdgeSeat extras are coverage-checked against the single passed feed and the seat still ticks once per user per feed, so a seat tick can no longer outlive a partial subscription). Duplicate group accounts in a batch are rejected. Old encodings without the count byte decode as 0, so existing clients are unaffected. Deploy ordering (RFC-1): the program must deploy to all clusters before any client that emits batches — an old program would misread the extra group accounts as the trailing optional accounts. (malbeclabs/infra#2114)
- SDK
- Append the payer Permission account on
doublezero feed create,feed delete,feed update,user delete,user update, and multicast role updates (UpdateMulticastGroupRoles, including the role-strip thatuser delete/request-banandfeed update|delete --force-unsubscriberun first) when that account exists and the serviceability program owns it. (malbeclabs/infra#2343) - The revdist fixture generator pulls
doublezero-program-toolsanddoublezero-revenue-distributionfrommalbeclabs/doublezero-solana, which moves there from thedoublezerofoundationorg. The pinned commit does not change, so the generator resolves the same code. CreateUserCommandandCreateSubscribeUserCommandtake an optional RFC-27ip_proof, which attaches the nativeEd25519SigVerifyinstruction the program looks for and sends both as one transaction, with the verifier key read fromGlobalState.ip_verifier_authority_pk. A proof naming a different owner, address, or user type is refused before the transaction is paid for; omitting it produces the pre-RFC-27 transaction unchanged. (#4200)DoubleZeroClientgainssend_instructions, for a transaction that needs more than one instruction.send_transactionis unchanged. (#4200)
- Append the payer Permission account on
- Utility crates
doublezero-serviceability-instructiongainsip_proof::ed25519_verification_instructionandip_proof::with_ed25519_verification, which lay out the Ed25519 precompile instruction for a proof and pair it with a user-creation instruction. With a proof attached aCreateUsertransaction still fits 10dz_prefix_blockaccounts andCreateSubscribeUser8. (#4200)
v0.37.0 - 2026-08-21
- Monitor
- The serviceability watcher's epoch estimates now use a per-chain slot time instead of 400ms everywhere: 350ms for Solana mainnet, 200ms for Solana testnet (which DoubleZero devnet also dials), and 400ms for the DoubleZero ledger. A 432,000-slot Solana epoch was over-estimated by ~6 hours on mainnet and ~24 hours on testnet in the
previous_epoch_start/next_epoch_startlog fields. The mainnet value must move to 200ms once Solana finishes its 200ms slot rollout. (malbeclabs/infra#2319)
- The serviceability watcher's epoch estimates now use a per-chain slot time instead of 400ms everywhere: 350ms for Solana mainnet, 200ms for Solana testnet (which DoubleZero devnet also dials), and 400ms for the DoubleZero ledger. A 432,000-slot Solana epoch was over-estimated by ~6 hours on mainnet and ~24 hours on testnet in the
- RFCs
- RFC-27: IP Ownership Verification Service for user connection
- Serviceability
GlobalStatecarriesip_verifier_authority_pk, the RFC-27 trust root for IP ownership proof validation, whichSetAuthorityanddoublezero global-config authority set --ip-verifier-authority <pubkey|me>rotate without a program upgrade. (#4196)CreateUserandCreateSubscribeUservalidate an optional RFC-27IpOwnershipProof, verified through the native Ed25519 precompile and signed byglobalstate.ip_verifier_authority_pk, so a caller can no longer bind aclient_ipit cannot originate traffic from. Enforcement is gated on the newrequire-ip-ownership-prooffeature flag: while it is clear a missing proof is accepted, and a supplied proof is validated in full either way. The sentinel authority may omit the proof, because the shred-oracle provisions users owned by validators and has no proof it could obtain; a proof it does supply is still validated (#4215). (#4197)
- IP verifier
- New
doublezero-ip-verifierservice signs the source address it observes as an RFC-27IpOwnershipProof, overPOST /v1/proof. Forwarded headers count only for connections from a--trusted-proxyCIDR, and only the--forwarded-headerthe proxy actually writes is read; the chain is walked from the right so a client-prepended hop is ignored. With no trusted proxies configured the connection peer address is the only address it will sign. Non-routable and IPv6 sources are refused, as is a request the cached ledger epoch is too old to answer. The verifier key is checked againstGlobalState.ip_verifier_authority_pkat startup and periodically after, so a rotation this service was not redeployed for takes it out of rotation instead of silently failing every user creation onchain. Built on axum, the first HTTP server framework in the Rust workspace. (#4198)
- New
- Utility crates
- New
doublezero-ip-proofcrate defines the RFC-27IpOwnershipProofand the exact bytes the verifier signs, in one place the serviceability program, the CLI, and the verification service all share. (#4195, #4206)
- New
v0.36.0 - 2026-08-14
- CI
- The devnet daily deploy waits for CloudSmith to list the nightly it just built before it runs ansible. On 2026-08-12 the client package finished uploading at 14:37:00 UTC, and four devnet hosts ran
apt-get updatebetween 14:37:21 and 14:37:24. Those four hosts read the old index and stayed on the 2026-08-11 nightly, while the four hosts that ansible reached 7 seconds later upgraded. The new step polls the jammy and noble package indexes for up to 10 minutes, then fails the deploy. CloudFront serves those indexes withmax-age=30, so the step waits another 35 seconds after the indexes list the version, because a host reaches a different edge than the runner. A component that builds no deb skips the step. (#4181)
- The devnet daily deploy waits for CloudSmith to list the nightly it just built before it runs ansible. On 2026-08-12 the client package finished uploading at 14:37:00 UTC, and four devnet hosts ran
- E2E/QA
TestQA_DeviceProvisioningreads the CLI's stdout on its own, rather than merging stderr into it.runCLIusedCombinedOutput, so the upgrade banner the CLI writes to stderr reached the JSON parser, and the devnet run on 2026-08-12 failed withinvalid character 'A' looking for beginning of value. The CLI's tracing diagnostics and ssh's host-key warning write to stderr as well, and each one broke the same parse. The test now logs that banner once per run as a warning. (#4181)
v0.35.0 - 2026-08-11
- CLI
doublezero feed getis removed, anddoublezero feed listgains--codeand--exchangefilters in its place.doublezero feed list --code shreds --exchange xlaxreturns the feed thatdoublezero feed get --pubkey <pubkey> --exchange xlaxreturned, plus thegroup_codescolumn the list view already carried. A code that matches no feed now prints an empty table instead of failing. Theexchangecolumn now carries the metro code, such asxlax, in place of the exchange pubkey, in both the table and the JSON. (#4171, #4172)doublezero feed updateanddoublezero feed deletenow name the feed as--pubkey <PUBKEY>, or as--code <CODE>with--exchange <EXCHANGE>.--pubkey shreds-laxused to accept a code and resolve it by reading every feed, which failed as soon as a second metro carried that code. Writedoublezero feed update --code shreds-lax --exchange xlax --name "Shreds LAX v2"instead. (#4172)
- CLI
doublezero access-pass listnow renders the groups a feed grants in themulticastcolumn, with anF:prefix next to the existingP:andS:entries. An EdgeSeat pass carries its multicast entitlement on its feeds, and the column read only the two allowlists, so such a pass showed an empty column and looked like it granted nothing.access-pass getgains afeedsrow and afeed_groupsrow, anduser getgains afeedsrow naming the feeds whose seats the user holds. Both name each feed ascode:metro— its code qualified by its exchange's code, e.g.lashay1-feed:xams— because a feed is keyed by(code, exchange), so a pass holding one code in three metros holds three different feeds and printing their codes named all three alike. A feed whose exchange account cannot be read falls back to the exchange key, which is uglier but keeps same-coded feeds apart; a feed that cannot be read at all still renders as its own key. The feed seats inaccess-pass get --jsonnow carryfeed_codeand the newexchange_codealongside the existingfeed_key, both unqualified, so nothing has to be split back out of a joined string.--multicast-group-subscriberand--not-multicast-group-subscribernow match a group a feed grants, so they agree with the column beside them. The publisher filters are unchanged, because a feed grants subscribe rights only. (malbeclabs/infra#2178, #4167, #4177)doublezero connect multicastwith no arguments now joins the feeds an EdgeSeat pass has purchased, instead of subscribing the host to nothing while reporting success. The no-arguments path resolves groups from the pass'smgroup_pub_allowlist/mgroup_sub_allowlist, which the feed purchase path never writes — only the administrative subscriber-allowlist instruction does — so a feed pass had both empty, printedThe AccessPass has no authorized multicast groups; nothing to connect to.and exited 0.doublezero-edge-connect's installers run exactly that form, so their|| warnguard could not fire and an install announced success while the host held no subscription. Connect now takes a seat on every purchased feed that has one free in the connected device's metro, and names each feed it skipped with the reason. Joining nothing is an error when the host holds no feed and could take none, whether the seats are full or serve another metro, because exiting 0 with nothing subscribed is indistinguishable from success to an unattended installer; a host that already holds a feed still succeeds and activates, so repeated installs stay safe. Headroom is measured againstFeedSeat.max_users, the fieldtry_add_feed_userenforces —max_future_usersis written and documented to flip the cap atwindow_endbut is read nowhere, so using it would propose feeds the program rejects. Candidate devices are restricted to the metros the purchased feeds serve, asresolve_feed_joinalready does, so a host that bought an Amsterdam feed while sitting nearest Frankfurt connects through Amsterdam rather than failing; a nearer device in a metro the pass holds nothing in is passed over, and an informational line names it with the latency difference. Only metros holding a feed with a free seat are candidates, so latency cannot land the host in a metro where everything it bought is already full. A user is keyed on(client_ip, user_type)with no device, so one host can still only ever hold one metro's feeds, and feeds in the others are named as skipped. When no eligible device serves any candidate metro the failure names those feeds, and any feed skipped as full, rather than reporting a metro problem for a cluster that simply has no activated devices. A--devicenaming a device other than the existing Multicast user's now fails rather than being silently ignored, matching the guardresolve_feed_joinalready applies. (#4173)doublezero feed createanddoublezero feed updatenow read back every--exchangeand--groupargument, so a feed cannot name a metro or a multicast group that the ledger does not carry. A base58 argument used to pass straight through with no read, so--group 4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4Tcreated a feed whose group nobody can join. A code was always read back, so only the pubkey form changes. (#4172)
- E2E/QA
TestQA_MulticastSettlement'svalidate_instant_allocation_price_matches_chainno longer names a specificdoublezero_solana_versionin its skip path. Both the comment and the skip message said the pin was0.5.10-1; testnet has since moved to0.5.11-1, so a reader was told the pin was merely behind when in factinstant_allocation_priceis in no release yet. They now name what actually gates the field — a doublezero-offchain release carrying doublezero-offchain#405 — and where the pin lives, neither of which goes stale as versions move. Comment and message only, no behaviour change.TestQA_AllDevices_UnicastConnectivityno longer counts a device that cannot accept users against its failure thresholds. Five sites already checkedactivated && max_users > 0and loggedIgnoring <x> failure for device not ready for users, but each incrementedFailedTestsbefore the check, so the carve-out suppressed only the log line while the device still counted as failed — and gating that counter alone would not have been enough, sinceSuccess()also requires a non-zero packet count a device that never connected cannot produce. Such devices are now excluded fromComputeFailureStatsentirely, per-host denominator included. This is what failed mainnet-beta QA three times over 2026-08-08/09:laconic-dfw-sw01,laconic-mia-sw01andlaconic-was-sw01have been activated atmax_users=0since 08-06, the client CLI refuses those connects outright, andcmh-mn-qa01draws from a 13-device pool, so three unusable devices read as a 21-29% per-host rate against the 20% gate. The excluded codes are now reported in test output, so a skip is distinguishable from a pass. The exclusion is deliberately a subset of the program'sis_device_eligible_for_provisioning: a device atusers_count + reserved_seats >= max_usershits the same CLI rejection and still counts as a failure, since narrowing that too would restore the capacity pre-filtering #3697 removed. A run that could not attempt more than half of the devices assigned to it — fleet-wide or on any single host — now fails (-skipped-threshold, default 0.5) rather than reporting green over the remnant, and testing nothing at all fails regardless of that threshold, where previously the rate was0/0andNaN > thresholdpassed silently. The gate is per host as well because a drained metro is a few percent of the fleet but all of one host's coverage. The count also publishes asdevices_skippednext todevices_testedin InfluxDB and ClickHouse, so a collapse that stays under the threshold shows up on the dashboard and not only in the test log. Separately, a ping that never gets a reply reports its packet counts rather thanfailed to ping after 3 retries: %!w(<nil>); the wrapped error was always nil, because the retry loop returns early on any real failure. (#4168)
v0.34.0 - 2026-08-07
- CLI
doublezero feed listgains agroup_codescolumn naming the multicast groups the feed holds, alongside the existinggroupscount. A group the ledger no longer carries renders as its raw pubkey. The JSON output gains the field as well. (malbeclabs/infra#2172, #4150)
- Collector
- The RIPE Atlas collector no longer re-exports the boundary result on every poll. RIPE's
?start=filter is inclusive and the collector passed it the newest timestamp it had already consumed, so each cycle re-fetched that result and appended a duplicate sample to the circuit's onchain account. Samples therefore accumulated at roughly twice the declared 600s sampling interval, and because a sample's time is derived asstart_timestamp + index × sampling_interval, the derived times outran wall clock — 403 of 435 mainnet pairs drifted more than an hour within epoch 196, worst case +43.7h, which madeevent_ts-keyed views unreliable and left consecutive epochs overlapping by days.wheresitupwas unaffected. Existing accounts keep their inflated sample counts; the drift stops accumulating from this change forward. Thetimestamp not updated (old results?)warning only fired because duplicates were being exported, so it would have gone silent with this fix; the stall signal is now keyed off the export cursor not advancing, and fires once a measurement has produced no new sample for over 30 minutes — whether it returned nothing at all or only timeouts. Expect thelatency_samples_per_collection_interval_missingcounter to start reporting on deploy: the duplicate was always a successful result, so every circuit exported at least one record every cycle and that counter could not fire at all. It is now accurate rather than newly broken, but with one probe per circuit and the sampling and export intervals both at 10 minutes, poll jitter will trip it intermittently in steady state — treat a sustained rise, not an isolated one, as signal. (#4154) - The hourly RIPE Atlas measurement cycle now reports source probes that have produced no successful sample since their measurement was created. A source's
last_response_atis zeroed whenever its measurement is created and is only ever advanced by a sample with a latency above zero, so a zero past a 2h grace period means that probe has contributed nothing to it — and because a circuit is enlisted exactly once (a measurement for targetTenlists only sources whose code sorts afterT), each such source silently darks its circuit with no other trace. Between 2026-08-05 and 08-07 this hit muc (11 circuits), then hkg (6), then dub (4), each stopping within the same second across every one of that probe's enlistments; they were found only by reading the state file by hand. New gaugedoublezero_internet_latency_collector_ripeatlas_sources_without_sampleslabelled bysource_location, plus a warning naming up to 20 affected sources per cycle; both are silent when there are none. The grace period is deliberately longer than the 1h probe timeout that drives probe rotation, and separate from it so the two can be tuned independently: RIPE dispatch is not immediate, an accepted enlistment has been observed producing its first result 80–100 minutes after creation, so a shorter window reports probes that are merely warming up. This is observation only — it does not mark probes or recreate measurements, because recreation invalidates every measurement whose target sorts before that metro and every drop observed so far recovered unaided. It also names no cause: a source RIPE never dispatched and a source whose path is at total packet loss are indistinguishable in this state, since a result carrying norttparses to zero latency. (#4155) - A RIPE Atlas result carrying no successful ping no longer looks like a dead source probe.
parseLatencyFromResultreturns zero for a result whose ping array holds nortt, and the caller skipped everything behind anif latency > 0gate, so a path at 100% packet loss never advanced that source'slast_response_at. It aged past the 1h threshold and the measurement cycle marked a probe that was uploading results on schedule as unresponsive — observed on mainnet-beta on 2026-08-06, where probe 7447 was marked 16 seconds after its most recent upload and the rotation recreated 28 measurements. A result the probe uploaded now counts as a response regardless of whether anything came back. What gets exported is unchanged: a total-loss result still writes no sample, so the export cursor still stalls when a whole measurement goes quiet and the target-level staleness check still fires. The trade is that a single source stuck at sustained total loss to one target, while its siblings keep returning RTTs, is no longer marked or rotated: itslast_response_atstays fresh and its siblings keep the export cursor advancing, sodoublezero_internet_latency_collector_latency_samples_per_collection_interval_missingfor that circuit is the remaining signal. That is deliberate — a wrongly-marked probe recreates every measurement whose target sorts before its metro — but it is a detector that used to exist. (#4153, #4160) - A failed internet-latency submission now retries from the first unwritten sample rather than restarting at the beginning of the flushed partition, and only the unwritten remainder is requeued — the same fix the device telemetry submitter got in #4145. A partition larger than one transaction is written in batches, so any failure part-way through left the earlier batches onchain while the retry and the next tick re-sent them, appending those samples a second time and skewing the latency they feed. Reachable today from an RPC timeout mid-partition; surfacing program rejections adds another way in. (malbeclabs/infra#1703, #4152)
- The RIPE Atlas collector no longer re-exports the boundary result on every poll. RIPE's
- SDK
- The Go telemetry SDK no longer reports a transaction the program rejected as a success. Finalization only means the cluster agreed on the transaction: a rejected instruction finalizes too, carrying the rejection in
err, which the executor never read. It now returns a*telemetry.ProgramErrorholding the ledger's error and the program's log output, and leads the message with the program's own explanation so a caller that just prints the error still gets the reason. This is the check the serviceability executor already made. (malbeclabs/infra#1703, #4152) - A samples-account-full or missing-account rejection that reaches execution now returns the same
ErrSamplesAccountFull/ErrAccountNotFoundthe equivalent preflight rejection does, via the newProgramError.CustomErrorCode(). Preflight catches nearly all of these, but a write that simulated cleanly and then failed against the bank it landed on reported its code only through the finalized transaction, so a caller's account-full handling worked on one side of preflight and not the other. (malbeclabs/infra#1703, #4152)
- The Go telemetry SDK no longer reports a transaction the program rejected as a success. Finalization only means the cluster agreed on the transaction: a rejected instruction finalizes too, carrying the rejection in
- Device Telemetry
- A submission the telemetry program rejects onchain no longer burns the tick's remaining attempts: the agent logs the rejection with the program's explanation at Error and moves on, counting
submitter_program_erroron the errors counter. Before this, the init half of the init→write path could not be seen to fail — it skips preflight, so the rejection only showed up on the finalized transaction, which the SDK read as success — and the agent looped init→write→account not foundevery few seconds with nothing in the log naming the cause. Observed on chi-dn-dzd4, where the device'smetrics_publisherhad been set to a key the agent did not hold. Samples are requeued as with any other failure, so the next tick retries once the cause is fixed. An init the program rejects because the account already exists is excepted: that leaves the write with what it needed, so the write now runs either way and only a write that still finds nothing there reports the init failure as the reason. (malbeclabs/infra#1703, #4152) - A ledger RPC outage no longer stops TWAMP probing on the device telemetry agent: the pinger caches the last known epoch and refreshes it off the probe path, instead of fetching it inline and skipping the tick on failure. Probing stops when no epoch has ever been fetched, when the cached one exceeds the new
-max-epoch-staleness(default 10h, clamped to what the sample buffer holds at-probe-interval), or when the cached epoch's projected end has passed. Samples taken against a cached epoch are written to that epoch's account, so a query scoped to a later epoch will not return them — the projected-end bound is what keeps that from spanning a rollover. The refresh cadence follows-probe-intervaland can be set with the new-epoch-refresh-interval. (#4143) - A peer discovery refresh that fails after reading the ledger no longer wipes the agent's peer list. It cleared the cache before calling
LocalNet.Interfaces(), so a transient failure there left the pinger iterating zero peers and probing nothing until a later refresh succeeded. The cache is now replaced only once the new list is built, which also shortens the critical section to the assignment. (#4146) - The telemetry agent now logs and counts samples it discards when a submission fails and the partition buffer is already at capacity; that path previously recycled the batch with no signal at all. New counter
doublezero_device_telemetry_agent_samples_dropped_totalwith areasonlabel (buffer_full), plussubmitter_buffer_fullon the existing errors counter. Requeue behavior below capacity is unchanged, and neither signal fires in steady state. (#4144) - Samples dropped because a partition's onchain account is full are now counted too, under
reason="account_full"plussubmitter_account_fullon the errors counter. That path reports success to its caller, so a warning was the only trace, and its count was wrong: it reported the whole flushed partition rather than the samples actually lost. (#4145) - A failed submission now retries from the first unwritten sample rather than restarting at the beginning of the flushed partition, and only the unwritten remainder is requeued. Previously a mid-partition error made every subsequent attempt re-send batches that were already onchain, appending those samples a second time and pushing the account toward the sample cap it is measured against. (#4145)
- Agent logs now identify the ledger RPC endpoint in use, and the peer count and the stale-program-data warning report state transitions instead of firing on every refresh. New lines name the resolved remote address of each connection (a bad load balancer address behind a hostname was invisible before), and a refresh that finds no peers is now a warning rather than a Debug line. The stale-cache warning also moves off the package-global
slogonto the agent's own logger, so it is formatted and leveled with everything else. New:doublezero_device_telemetry_agent_peersgauge, andpinger_epoch_fetchon the errors counter for every exhausted epoch fetch. (#4147)
- A submission the telemetry program rejects onchain no longer burns the tick's remaining attempts: the agent logs the rejection with the program's explanation at Error and moves on, counting
- QA
- Rework existing TestQA_MulticastSettlement and adapt it to the new
FLAG_RETRANSMIT_ONLY_ONBOARDING_ENFORCED_BITflag. Test now checks for this flag in the ProgramConfig solana account and depending on if it's on or off tries to assert that no new user can subscribe to a non retransmit-only metro unless that metro has the retransmit-only flag enabled in the MetroHistory account. (#4156)
- Rework existing TestQA_MulticastSettlement and adapt it to the new
v0.33.0 - 2026-07-31
- CLI
doublezero feed update(with--group) anddoublezero feed deletenow fail closed if the change would leave an EdgeSeat user holding a multicast group role that no consumed feed seat on their access pass covers and the pass's own allowlists don't authorize, printing the affected users, groups, and roles and submitting nothing. Pass--force-unsubscribeto remove those roles first and then apply the change; a group where one role must go while the allowlist still authorizes the other fails closed even with the flag and needs manual cleanup. A rename or an update without--groupis unaffected; an additive--groupset scans but finds nothing to do. The removals needUSER_ADMIN(or foundation membership) on the payer in addition toFEED_AUTHORITY. (malbeclabs/infra#2113, #4119)
- CLI
doublezero connect multicastgains--subscribe-feedand--unsubscribe-feed(feed codes or pubkeys) for EdgeSeat access passes: one command creates the bare multicast user if needed and joins whole feeds, or leaves them. When both flags are given the leave runs first, and if one half fails the output names it and says which flag to rerun. (#4111)
- SDK
- New
SubscribeFeedCommandandUnsubscribeFeedCommandin the Rust SDK derive the exact group lists the onchain instructions demand and check the metro, pass, and feed-cap constraints before sending; the daemon-cliLedgerClientgainslist_feed,subscribe_feed, andunsubscribe_feed. (#4111)
- New
- Controller
- A multicast user with no publisher role now gets the subscriber ingress ACL and a deny-all announce prefix-list; a user with no roles at all previously fell through to the publisher versions of both. (#4110)
- Serviceability
- New
SubscribeFeedandUnsubscribeFeedinstructions join or leave whole feeds on an EdgeSeat access pass in a single atomic transaction, charging one seat per feed rather than per group. A feed is all-or-nothing: the caller names feeds, the processor derives which groups change and rejects a group list that does not match, so two feeds carrying the same group stay unambiguous.UpdateMulticastGroupRolesnow enforces the multicast-group allowlists for every access-pass type, EdgeSeat included, so purchased groups go through the feed instructions and individually comped groups through the allowlist.CreateSubscribeUserskips the allowlist only for the case its feed gate actually covers, closing two paths that could join a group with no check.MAX_FEED_GROUPSdrops from 64 to 20, bounded by what one transaction can carry: because joining passes every group a feed holds, a larger feed could never be joined. No feed onchain is affected. For the same reason a user may hold at most 5 feeds (a leave names every held feed plus the departing groups behind the client's compute-budget prelude), and a held feed the pass no longer carries is pruned on leave instead of blocking it. New errors:EdgeSeatRequired(101),UserDeviceMismatch(102),UserFeedLimitExceeded(103),EdgeSeatIsMulticastOnly(104). (#4109) CreateUsercan now create a bare multicast user (no group, no feed) under an EdgeSeat pass: the feed gate moved toCreateSubscribeUser, its only caller, and feed seats are otherwise charged bySubscribeFeed. The pass-levelmax_multicast_userscap is enforced again (it had been vestigial under the feed-seat model) so bare users stay bounded; the oracle already provisions it as the total purchased seats.CreateUseris also idempotent: re-running it for an existing user matching the requested owner, device, user type, and tenant succeeds without changing state, so a client can retry safely; a mismatch still fails withAccountAlreadyInitialized, and a banned user withInvalidStatus. This applies to the current client_ip-keyed user PDA; the legacy index-keyed PDA cannot be re-run, since its index has moved on. Deploy note:max_multicast_usersbecomes load-bearing on existing EdgeSeat passes; before deploying to a cluster, confirm each pass's value covers its feed seats and backfill viaSetAccessPasswhere short. Checked 2026-07-30: mainnet-beta and devnet hold no EdgeSeat pass; testnet's single one (5x9DTsWC…) hasmax_multicast_users1, zero users, zero connections. (#4110)
- New
- CI
- The Rust toolchain moves from 1.90.0 to 1.97.1, seven releases forward. 1.90.0 sat below the supported floor of the rust-analyzer builds shipping with current editors (1.94.0), so the language server warned on every workspace load; that floor tracks stable at roughly minus-three releases, so the gap widens with each Rust release rather than holding steady.
rust-toolchain.toml, the twelvedtolnay/rust-toolchainCI pins across seven workflows, the devcontainer Rust feature, and the rustup pin baked intorelease/Dockerfile.releasemove together — contributors must rebuild their devcontainer after this merges. The release image needs no manual step:scripts/build-snapshot.shkeys its rebuild on the Dockerfile's hash, so changing the pin is what triggers it. Left stale, that image would have kept 1.90.0 while goreleaser ran against a 1.97.1rust-toolchain.toml, re-downloading a full toolchain on every snapshot build (RUSTUP_HOMEis not one of the named volumes and the container runs--rm). The accompanying source changes are only the clippy lints the newer toolchain adds (useless_borrows_in_formatting,unnecessary_sort_by,iter_kv_map,to_string_in_format_args,collapsible_match,result_large_err). The serviceability ones are redundant&removals insideDisplay/Debugimpls, whereformat_args!auto-references, so formatted output is byte-identical; they were required rather than optional becauselint-programsruns fromsmartcontract/and so picks up the root toolchain, notsmartcontract/programs/rust-toolchain.toml. Onchain codegen is unaffected either way:cargo build-sbf --tools-version v1.54supplies its own rustc. (#4118) dispatch-and-waitnow reports to Slack which jobs of a dispatched cross-repo run did not pass, each with its conclusion, so the release thread says what broke instead of only that something did. It names every concluded job that is notsuccessorskipped— a leg that exceeds itstimeout-minutesconcludestimed_out, notfailure— and it fires when the calling job is cancelled or hits its own timeout as well as when the run fails, which is where a stalled stage would otherwise go unannounced. The report is best-effort: it warns and exits 0 on a token or API failure, so it can never mask or replace the failure the calling job already reports. The job list is capped at 10 names with an overflow count. Wired up on the orchestrator'sqajob (the infraqa.testnet.ymldispatch), which now also listspreflightdirectly inneedsto read itsthread_ts. (#4121)
- The Rust toolchain moves from 1.90.0 to 1.97.1, seven releases forward. 1.90.0 sat below the supported floor of the rust-analyzer builds shipping with current editors (1.94.0), so the language server warned on every workspace load; that floor tracks stable at roughly minus-three releases, so the gap widens with each Rust release rather than holding steady.
v0.32.0 - 2026-07-29
-
SDK
- The Rust SDK no longer attaches the payer's Permission PDA to serviceability transactions. The RFC-26 builders derive account layout offline and their Permission append is still deferred, so every migrated instruction reaches
authorize()with no Permission account and takes the legacy GlobalState allowlist/authority path. A payer whose only grant is an activated Permission account — not onfoundation_allowlist/qa_allowlist, not a matching authority key — getsNotAllowedon every gated command. Re-enabling the append is per-instruction future work and must be sequenced with the permission-model rollout. Verified against live chain state at time of writing, no cluster is exposed:RequirePermissionAccountsis off everywhere (so the legacy fallback is live), mainnet-beta's 8 Permission accounts all grantFOUNDATIONto keys already onfoundation_allowlist, and the one grant on testnet/devnet that no legacy authority covers (ACCESS_PASS_ADMINtoEWFXDTBWhNxsmj4mbahZL7HH1XpmC6Ao3LZUhJGCyyPh) belongs to the shred oracle, which assembles its serviceability instructions itself and appends its own Permission account — it never uses this send path, so it is unaffected. Notedoublezero permission auditdoes not answer this question: it reports the inverse (strict-mode) direction only, so checking it means diffingpermission listagainst the GlobalState allowlists. (#4060)
- The Rust SDK no longer attaches the payer's Permission PDA to serviceability transactions. The RFC-26 builders derive account layout offline and their Permission append is still deferred, so every migrated instruction reaches
-
Serviceability
MIN_COMPATIBLE_VERSIONmoves from0.21.0to0.30.0, excluding every client that predates theEdgeSeat(Vec<FeedSeat>)AccessPassdecoder (#3954, #4030). Such clients misparse every field after the variant tag and can abort on the bogus allowlist length that follows; testnet already holds an EdgeSeat pass, so this is a live crash rather than a hypothetical one. The oldest release this admits is v0.31.0, not v0.30.0: theclient/v0.30.0tag was cut before its version-bump commit merged, so its binaries self-report0.29.0— indistinguishable from the genuine v0.29.0 release, which has no EdgeSeat decoder — and a floor low enough to admit them would admit v0.29.0 too. The v0.30.0 release is therefore excluded by its version string rather than by any real incompatibility (its code has the decoder). Tag/version drift is fixed for v0.32.0 onward by #4068. The floor is stamped ontoProgramConfigbydoublezero init, so it takes effect per cluster at that cluster's next program deploy — not at merge — at which point the window is[0.30.0, <deployed version>], admitting v0.31.0 and the release being deployed. A client below the floor fails at startup with "A new version of the client is available".Useraccount: the single-feed slotfeed_pkis replaced in place byfeed_pks: Vec<Pubkey>across the Rust struct, serde JSON, and all SDKs (GoFeedPk→FeedPks, TypeScriptfeedPk→feedPks, Pythonfeed_pk→feed_pks). Consumers reading the old key must update. The vec occupies the former 32-byte scalar slot; this is safe because no account ever recorded a feed there (no EdgeSeat access pass has existed on any cluster — verified against the mainnet-beta data lake), so existing accounts carry 32 zero bytes that parse as an empty vec with the leftover zero bytes ignored as trailing data. Deploy note: confirm no nonzerofeed_pkaccount exists on each cluster at upgrade time; an EdgeSeat multicast connect before the upgrade would record a feed the new layout cannot see. (#4080)
- CLI
doublezero user create-subscribegains a--feedflag (a Feed pubkey or an unambiguous feed code) passed as the trailingFeedaccount the EdgeSeat feed metro gate requires. Without it, connecting a Multicast user to a feed pass failed withFeedAccountRequired. (#4087)
- Serviceability
Useraccounts can hold EdgeSeat feed seats on multiple feeds:feed_pks: Vec<Pubkey>records every feed whose per-feed seat the user consumed at connect, and delete releases all of them. Added to the Go/TypeScript/Python SDKUserdecoders as well. (#4080)- Bound the preallocation in
deserialize_vec_with_capacityagainst the remaining input. A garbage or attacker-controlled u32 length prefix in an account (e.g. a pre-FeedSeat SDK misparsing an EdgeSeat AccessPass) could request tens of GiB viaVec::with_capacity, aborting the process through the uncatchable alloc-error handler; the capacity is now capped at the remaining byte count. Decoding of valid accounts is unchanged. (#4072) - Reject duplicate entries in a Link's
link_topologies, and decrement each topology'sreference_countonce per unique entry when a link drops it — onLinkDeleteand on theLinkUpdateremoval path.LinkUpdatediffs old vs new as a set, so a duplicate (--link-topology "TOPO-A,TOPO-A", one comma typo by aNETWORK_ADMINor foundation key) stored two entries against a single increment while both drop paths decremented per entry — which could zero areference_countanother link still contributes to and letTopologyDeleteclose the topology PDA that guard protects, leaving a dangling pubkey in the surviving link and a permanently retired admin-group bit. The duplicate check is on the instruction argument, deliberately not inLink::validate():validate()runs insidetry_acc_write, so a link already holding duplicates would become unwritable and therefore unrepairable. No cluster carries existing skew — mainnet-beta has no link, current or in any historical snapshot, with more than one topology entry. (#4090) AccessPassgains adzf_lockedflag (bit 0 offlags) marking a pass as foundation-managed so automated reconcilers such as the Feed Oracle leave it alone. A newSetAccessPassFlagsinstruction (gated onACCESS_PASS_ADMIN) sets/clears access-pass flags without disturbing the others, andSetAccessPassnow preserves the flag across unrelated updates. Set it with thedoublezero access-pass dzf-lock/dzf-unlockverbs oraccess-pass set --dzf-locked. (#4083)
- CI
- The daily release build populates the Go module cache in a retried step before GoReleaser runs.
proxy.golang.orgoccasionally drops a module zip mid-transfer (HTTP/2INTERNAL_ERROR) and the go command does not retry, so the failure previously surfaced as a GoReleaser build error partway through the release. Retrying up front is cheap and side-effect free. The step is best-effort and bounded (continue-on-error, 10-minute timeout): it fetches a superset of what the released binaries import, so a durable proxy failure on a test-only dependency, or a stalled transfer, degrades to the previous behavior — GoReleaser fetches whatever is still missing — instead of becoming a new way for the release to fail or hang. (#4105) - Add Copilot code-review instructions: a repo-wide
.github/copilot-instructions.md(review posture plus cross-cutting rules) and eight path-scoped.github/instructions/*.instructions.mdfiles covering the onchain programs, the Rust SDK/builders/CLI, Go, E2E/QA, release workflows, the language SDKs and their golden fixtures, tests, and the changelog. Rules were extracted from the team's own inline PR reviews over the previous month and cross-checked against the code, plus the standards in RFC-1 (semver classification, deploy-ordering prerequisites), RFC-10 (compatibility-window schema evolution), and RFC-20. No effect on builds or runtime. (#4093)
- The daily release build populates the Go module cache in a retried step before GoReleaser runs.
- Client daemon
- A device being added to or removed from the fleet no longer tears down and recreates every DoubleZero user's tunnel.
DoubleZeroPrefixesin aProvisionRequestis the union of every device'sdz_prefixesfleet-wide, so one device changing shifts it for everyone, yetEqual/InfraEqualcompared it for all user types and pushed the reconciler onto the full-reprovision path. Deleting the Linux interface drops every multicast group membership bound to it — consumers that join only at startup silently stop receiving traffic while BGP reconverges and all DZ-side health checks pass. The comparison is now scoped toUserTypeEdgeFiltering, the field's only consumer (EdgeFilteringService.createIPRules); IBRL and multicast never read it.Diffstill reports the prefix-count change unconditionally so the log line stays useful for diagnosis. (#4102, malbeclabs/infra#2117) - Route liveness recovers from a transient loss in roughly one backed-off probe interval plus two round trips, instead of up to three such intervals. A state-changing RX now advertises the new state immediately and re-arms the normal transmit cadence, rather than waiting out the timer armed while the session was Down (exponentially backed off up to
-route-liveness-backoff-max). Two defects contributed: the RX path never enqueued a TX on a state change, and the scheduler's sleep had no wakeup path, so an event pushed earlier than the deadline it was sleeping on could not shorten that sleep — which also delayed the first probe of a newly registered route by up to another peer's backed-off interval. The first probe after the path recovers is still bounded by the backed-off interval, so thebackoffMaxdefault (60s) still sets the floor on recovery time; that default is unchanged here. The immediate transmit takes over the pending-TX slot rather than adding to it, so recovery re-arms the normal cadence instead of leaving the session silent until the stale backed-off deadline — which would have made the peer time it back out and flap the route every detect interval. It is also paced by the transmit interval measured from the last send, so a peer whose inbound traffic is blocked and which therefore repeatsDown(flipping the session Down↔Init on every packet) cannot drive the transmit rate 1:1 with its send rate pastminTxFloor/maxTxCeil; and a displaced transmit event is now dropped as stale when it pops, as detect events already were, so a takeover emits no duplicate packet and leaves no backlog. Also logs the effectiveminTxFloor/maxTxCeil/backoffMaxat startup, since a wrong value was otherwise only visible as slow reconvergence. (#3935)
- A device being added to or removed from the fleet no longer tears down and recreates every DoubleZero user's tunnel.
- Device controller
- Escalate onchain account fetch failures to
ERRORonly when sustained; a transient blip that recovers on the next poll now logs atWARN, so a single flaky fetch no longer pages via the generic ERROR-level alert. A weighted score (+1 per failure, -0.5 per success, floored at 0, capped at 6) crosses the threshold on a persistently failing endpoint, so real outages still surface. Each fetch is bounded by a 30s timeout so a hung endpoint fails the tick promptly rather than blocking for minutes. (#4081)
- Escalate onchain account fetch failures to
- Tools
- Treat truncated or partial JSON-RPC response bodies (
unexpected end of JSON input,unexpected EOF) as retryable, so a cut-off 200 response is retried in-call; genuinely malformed but complete responses remain non-retryable. (#4081) - Retry HTTP 429/500/502/503/504 from a Solana JSON-RPC endpoint. The classifier asserted on
StatusCode() int/Code() intinterfaces that no solana-go error type implements — both expose their code as a struct field — so the entire status-code branch was dead and every Go ledger reader gave up after one attempt. On 2026-07-28 that meant 4+ hours of 100% fetch failure across ~20 components against a healthy chain, absorbed only because each happens to keep a last-good cache. Classification now reads the concrete*jsonrpc.HTTPErrorand*jsonrpc.RPCErrorvalues, covering both the undecodable-body case and the decodable JSON-RPC error envelope the incident actually arrived in, and matches transient wording from providers that set no machine-readable code. Backoff is jittered so the ~60doublezerodhosts and dozen services reading one endpoint do not retry in lockstep against an endpoint already shedding load.sendTransactionandrequestAirdropare never retried regardless of the options passed, so no caller can resubmit a transaction the endpoint already accepted. Newdoublezero_solana_rpc_retries_totalanddoublezero_solana_rpc_retries_exhausted_totalcounters (labelled by method) make the next episode visible without reading logs. Only codes that a later identical request can plausibly succeed on are retried —-32005(node unhealthy),-32004(block not available) and the provider-minted-32429(rate limit);-32003(transaction signature verification failure) is deterministic and is not retried, so a bad signature fails on the first attempt instead of spending the whole budget against the endpoint. (#4098) - Consolidate Go Solana RPC construction on
tools/solana/pkg/rpc.New, replacing the duplicated transport-level retry loops insdk/shreds,sdk/revdistand the internet-latency collector (each ignoredctxand slept a fixed unjittered backoff) and adding retries to the ledger readers that had none:doublezerod, monitor, funder, device-health-oracle, telemetry (data-api, geoprobe-agent, telemetry including its netns/on-device path, data CLI), cdiff and the Go SDK. Note for consumers ofsdk/shredsandsdk/revdist: the retry budget contracts from 6 attempts over ~30s of unjitteredtime.Sleepto 4 attempts over ~3s that honorctx, so a blip longer than a few seconds now surfaces as an error rather than blocking for half a minute — passRetryviatools/solana/pkg/rpc.Newfor a longer budget. ~30s is the established budget convention for QA-test callers in this codebase, so those callers should passRetrywith a largerMaxAttemptsexplicitly rather than silently inheriting the shorter default. The shared transport's default per-request timeout drops from 5 minutes to 10s, which also bounds a single retry attempt: retry multiplies a hung endpoint by the attempt count, so the previous default let one logical call consume up to 20 minutes on the readers that inherit it (doublezerod, monitor, funder, device-health-oracle, telemetry, data-api, cdiff, state-ingest, flow-enricher, global-monitor, the Go SDK andsdk/revdist). 10s leaves more than an order of magnitude of headroom over the heaviest call we make — an unfilteredgetProgramAccountsover the serviceability program, ~2.9MB raw / ~1.0MB gzipped and ~0.4s against mainnet — and keeps an exhausted budget inside the tightest unbounded caller cadence (state-ingest's 60s refresh);sdk/shredsand the internet-latency collector keep their explicit 15s. The dial timeout drops from 5 minutes to 10s, matching its TLS handshake bound — a dial that can hang for minutes makes any retry budget meaningless. (#4098)
- Treat truncated or partial JSON-RPC response bodies (
- E2E/QA
TestQA_MulticastSettlementskips (with anexpected epoch-tail closed window: ...message) instead of failing whenwait_for_open_phasetimes out during the by-design closed window at the tail of every Solana epoch. The classification is verified against live chain state — theclosed_for_requests_grace_period_slotsread from the shred-subscription ProgramConfig, the execution controller phase and last-close slot, and the epoch schedule from the target cluster's RPC — and requires the whole timed-out wait (not just its end) to fall inside the window, so nothing is hardcoded and a timeout outside the window still fails as loudly as before. (#4069)TestQA_MulticastSettlementrecovers from the failure modes that kept mainnet-beta QA red:ensure_multicast_disconnectedself-heals seats left stuck-active onchain by a previous run's failed withdraw (scanning client seats for the client's public IP via the shreds SDKFetchAllClientSeatsand withdrawing any withTenureEpochs > 0), and every withdraw — thewithdraw_seatstep, the self-heal, and the cleanup — retries over a bounded window instead of failing on a single spurious "request in flight" preflight bail. The retry rotates to a different Solana RPC endpoint on the in-flight bail (the stalegetMultipleAccountsread behind it is per-endpoint) and confirms completion against fresh onchain state rather than the CLI's error text. Thewait_for_seat_allocation_ackedstep is removed: polling the seat's pending flag cannot distinguish a fast ack from a re-fund of an active seat that never creates a request; the retrying withdraw instead confirms completion against the seat's onchain tenure and pending-request state. (#4066, supersedes #4065)- Add
TestQA_RetransmitOnlySettlement, an E2E QA test demonstrating retransmit-only feed subscription and settlement via shred pay. (#4077) TestE2E_MultiClientIBRL_RouteLivenessdiagnoses its own route-convergence timeouts:requireEventuallyRouterenders the pass number (itspass %dmessages previously printed literally) and dumps the failing client's daemon/routesview, liveness counters and iptables INPUT counters instead of onlyCondition never satisfied. Clients 1-3 run with route-liveness debug logging. (#3935)
- SDK
- Add the
doublezero-serviceability-instructioncrate (RFC-26): pure, RPC-freebuild_xxx(...) -> Instructionbuilders — one per buildable serviceability instruction — that assemble account layout and borsh-pack args offline (SPL-style), with the trailing-account convention centralized incommon::build. Backed by goldenix_*fixtures (CI drift-guarded) and asolana-program-testsuite that runs the highest-cardinality builders (create_device,create_link,create_subscribe_user, atomicdelete_device, andclear_topology) against the real program to catch account-order drift. - Migrate every serviceability
commands/*execute()to delegate to those builders and a single newDoubleZeroClient::send_transaction(compute-budget prelude + sign + send), replacing the fourexecute_*(instruction, accounts)methods and the client-side account assembly. Commandexecute()signatures/returns are unchanged, so CLI/sentinel/daemon consumers are unaffected. The_quietsend variant and itsSimulationError/SimulationTransactionErrortypes are dropped: their only caller was theactivator/crate, deleted in #3647. (#4060) - Fix
doublezero topology clearreverting with "Topology Account is not writable" on a live topology. The SDK passed the topology PDA read-only, but the processor asserts it writable whenever it actually clears a link reference (it decrementsreference_count), so the command only worked on the already-closed-topology path — blockingtopology delete, which requiresreference_count == 0. The RFC-26clear_topologybuilder passes the account writable, which is a strict superset privilege, so no other path changes. (#4078)
- Add the
v0.31.0 - 2026-07-17
- CI
- Fix the event-driven
doublezero-edge-connectrebuild: the base-image publishers now mint a short-lived, least-privilege token from the release-bot GitHub App (scoped todoublezero-edge-connect) to fire the cross-reporepository_dispatch, replacing the never-createdEDGE_CONNECT_DISPATCH_TOKENsecret whose absence silently no-op'd every notify. Also add the missing devnet notify step, so all three variants (testnet, mainnet-beta, devnet) trigger a rebuild on base publish. The mint/notify steps stay non-fatal to the publish and edge-connect's daily poll remains the fallback. - Collapse the testnet release tag phase to a single approval: gate 1 now confirms the version-PR merges and authorizes the tag push in one click. The reusable tag workflow no longer carries an environment (it is pure mechanism); the approval prompt lives with its callers — the orchestrator's gate 1, and a new
approvejob (ontestnet) in the manual components dispatcher, so manual tag pushes prompt exactly as before — and agithub.workflow_refcaller allowlist makes any other caller fail closed. No new environments. Gate job display names and the Slack posts now spell out what each approval means and that gate 1 must only be approved after both PRs merge. - Add a
pipeline-completeguard job (if: !cancelled(), needs every stage throughqa) to the testnet release orchestrator that fails unless every stage concluded success, so a run that silently skipped stages can never end green; hollow successes also trigger the Slack failure alert. Deliberately cancelled runs stay quiet, andannounceis excluded so an announce-only failure can't fire a "release failed" alert for a release that succeeded. - Guard the version-bump script against resolver side effects:
cargo update --workspacemay only add the members' new version lines toCargo.lock— any other added line (e.g. a dependency-edge rebind like the observedsolana-system-interface3.2.0→2.0.0 flip) fails the bump before a poisoned PR is opened; pure removals (stale-entry pruning) are logged for the reviewer.
- Fix the event-driven
- CLI
- Documentation only: update
docs/cli-standard.mdto reference thedoublezero-daemon-climodule crate (final PR of the RFC-20 daemon-cli extraction stack). The transition shims (client/doublezero/src/servicecontroller.rs,client/doublezero/src/command/) were already removed by the earlier stack PRs; the binary is unchanged. (#4047) - The
feedverbs accept an exchange code for--exchangeand multicast group codes for--group, in addition to pubkeys; pubkey inputs behave exactly as before. (#4027)
- Documentation only: update
- Serviceability
- Add the
doublezero-serviceability-instructioncrate (RFC-26 R0): pure, RPC-free instruction builders for the serviceability program (the SPLinstruction::*pattern). R0 ships the scaffold, the shared trailing-account keystone, and four exemplar builders (create_device,create_link,delete_device,create_subscribe_user). Nothing consumes it yet; remaining domains and golden-fixture /solana-program-testcoverage land in follow-up PRs. (#4049) access-pass get --jsonincludes afeed_seatsarray exposing each EdgeSeat pass's per-feed seat state (user counts and billing windows); the table view is unchanged. (#4063)
- Add the
v0.30.0 - 2026-07-10
- Serviceability
Feedaccount: a catalog entry for one metro's multicast group set, keyed by(code, exchange)(onefeed_keyis one feed in one metro), managed by a catalog admin (FEED_AUTHORITYPermission orFOUNDATION) viaCreateFeed/UpdateFeed/DeleteFeed. (#3953)SetAccessPassFeedsprovisions feed_keys (SKU seats) onto an EdgeSeat pass, eachFeedSeatcarrying the feed's full per-feed billing state (current cap, future cap, the window boundary between them, the termination date, and the renewal anniversary day); the oracle calls it via itsACCESS_PASS_ADMINPermission. (#3954, #4030)- The
AccessPassEdgeSeatvariant now carries aVec<FeedSeat>payload (feed_key+ per-feed cap) instead of being a bare marker. This changes theAccessPassborsh layout for EdgeSeat passes. (#3954) - EdgeSeat multicast connect is metro-gated: a device whose exchange is not covered by any of the pass's feeds is rejected with
MetroMismatch, and the matching feed's per-feed cap is enforced. (#3955)
- Controller
- Track the latest config agent version per device in a new
controller_agent_versionsClickHouse table, updated on GetConfig polls. (#3578)
- Track the latest config agent version per device in a new
- SDK
- Give the shreds SDK RPC client a bounded per-request timeout (15s) and a sized connection pool instead of the unbounded
http.DefaultClient, so a slow or degraded RPC endpoint fails fast rather than blocking until a transaction's blockhash expires and its send fails preflight withBlockhashNotFound. - Go, TypeScript, and Python deserialization for the
Feedaccount and theEdgeSeatFeedSeatpayload. (#3956, #4030)
- Give the shreds SDK RPC client a bounded per-request timeout (15s) and a sized connection pool instead of the unbounded
- Serviceability
- Gate Device and device-interface instructions on
NETWORK_ADMIN(andHEALTH_ORACLEfor sethealth) or the contributor owner viaauthorize(); internal foundation-only sub-gates now also accept NETWORK_ADMIN holders. (#3980) - Gate UpdateUser on
USER_ADMIN, CheckAccessPass onACTIVATOR, and accesspass CheckStatus onACTIVATOR|USER_ADMINviaauthorize(); user create and set_bgp_status remain owner-authorized (not part of the admin Permission system). (#3984) - Gate Tenant instructions (create/update/delete/add_administrator/remove_administrator/update_payment_status) on
TENANT_ADMINor foundation/sentinel viaauthorize(). (#3983) - Gate MulticastGroup CRUD on
MULTICAST_ADMINand publisher/subscriber allowlist add/remove onmgroup.owner OR MULTICAST_ADMIN/ACCESS_PASS_ADMINviaauthorize(); add handlers use split_trailing_permission. (#3982) - Require a Permission account (or the legacy foundation authority) for GlobalState, GlobalConfig, and foundation/QA allowlist admin instructions, gated on
GLOBALSTATE_ADMINviaauthorize(). (#3977) - Gate Contributor instructions (create/update/suspend/resume/delete) on
CONTRIBUTOR_ADMINor foundation; the contributor owner retains the ops-manager-only update path. (#3978) - Gate Location and Exchange instructions (create/update/suspend/resume/delete, exchange setdevice) on
INFRA_ADMINor foundation viaauthorize(). (#3979) - Gate Link instructions (create/update/delete/suspend/resume/accept/sethealth) on
NETWORK_ADMIN(andHEALTH_ORACLEfor sethealth) or the contributor owner viaauthorize(); variable-length delete/update use split_trailing_permission. (#3981)
- Gate Device and device-interface instructions on
- Collector
- Harden ledger writes against a slow/degraded RPC endpoint: bound each RPC request (default 15s,
--ledger-rpc-timeout), size the connection pool above the submitter concurrency (default 128,--ledger-rpc-max-conns), and deadline each submission attempt so it fails fast and retries with a fresh blockhash instead of sending an expired one and failing preflight withBlockhashNotFound. (#3973)
- Harden ledger writes against a slow/degraded RPC endpoint: bound each RPC request (default 15s,
- Onchain programs
- Restrict granting the
FOUNDATIONpermission flag: a plainPERMISSION_ADMINholder can no longer grantFOUNDATION(a privilege escalation). Only afoundation_allowlistmember or an existingFOUNDATIONholder may grant it, enforced independently ofRequirePermissionAccountsso foundation members are never locked out.
- Restrict granting the
- CLI
- Move the multicast transport verbs (
multicast subscribe/unsubscribe/publish/unpublish) into thedoublezero-daemon-clicrate per RFC-20. Each now takes&CliContext+ generic&D: DaemonClient+&L: LedgerClient+&mut Wwriter; informational/result lines route through the shared writer (stdout, previously the stderr spinner). They stay nested underdoublezero multicast(not hoisted as top-level daemon verbs); onchainmulticast groupCRUD is unchanged. The binary'scommand/helpers.rs(resolve_client_ip) and theservicecontroller.rsremnant are deleted — the crate copies survive. Flags, output content, and exit codes are unchanged. (#4037) - Move the
connectverb into thedoublezero-daemon-clicrate per RFC-20. It now takes&CliContext+ generic&D: DaemonClient+&L: LedgerClient+&mut Wwriter; informational/result lines route through the shared writer (stdout, previously the stderr spinner), spinners stay on stderr, pre-flight diagnostics route throughtracing, and device selection uses the crate's latency utilities (from #3995). The binary'sdzd_latency.rsand the orphanedcheck_doublezeropre-flight are deleted. Flags, output content,--verbose, exit codes, and version-check semantics are unchanged. (#4010) - Move the
disconnectverb into thedoublezero-daemon-clicrate per RFC-20. It now takes&CliContext+ generic&D: DaemonClient+&L: LedgerClient+&mut Wwriter; informational/result lines route through the shared writer, spinners stay on stderr, and diagnostics route throughtracing. Behavior (flags, output,--verbose,--no-wait, version-check) is unchanged. (#4008) - Move the
latencyandroutesverbs, plus the device-selection/latency-polling utilities andresolve_client_ip, into thedoublezero-daemon-clicrate per RFC-20. Both verbs now take&CliContext+ generic&D: DaemonClient+&L: LedgerClient+&mut Wwriter; diagnostics route throughtracingand output through the shared writer helper. Behavior (flags, output,--json, version-check) is unchanged. (#3995) - Add
doublezero permission auditto check legacy→Permission parity before enablingrequire-permission-accounts: it reports which legacy keys would lose access to migrated instructions (coverage gaps, non-zero exit), super-admin holders, and the non-migrated subsystems that still depend on the GlobalState allowlists. - Block
doublezero device deletewhen the device is still enabled in the shred-subscription program (checked via itsDeviceHistoryaccount on Solana L1), preventing an orphaned device that deadlocks shred oracle epoch settlement. (#3989)
- Move the multicast transport verbs (
- E2E
- Fix the multicast settlement QA test's seat-allocation ack wait. It read the reused client seat at finalized commitment and could accept the previous run's already-acked state, then withdraw while the current request was still pending. It now waits to observe the request pending before treating a cleared flag as the ack. (#3972)
- Sentinel
- Enable the
reqwestjsonfeature explicitly for the sentinel crate, which relies onResponse::json()in the validator metadata reader and multicast publisher after the workspace dependency stopped enabling it by default. (#3986) - Make the all-devices unicast QA test tolerate a host reporting multiple tunnel statuses. It now selects the IBRL status via
GetUserStatuses/FindIBRLStatusinstead of erroring on a lingering Multicast tunnel and dropping the host, and logs a warning when a host reports more than one status. (#3976) - Add a
doublezero feedCRUD lifecycle test (create/get/list/update/delete) against a live devnet. (#3994)
- Enable the
- CI
- Auto-publish the mainnet-beta client base image daily when the stable Cloudsmith channel advances (idempotency-gated so a run with no new version is a no-op), and notify
doublezero-edge-connectto rebuild its testnet and mainnet-beta variants when a new base image is published. Serialize publishes with per-job concurrency groups, keep the notify steps non-fatal to the publish, and skip Debian pre-release versions when resolving the mainnet-beta tag. (#3990) - Add a testnet release orchestrator workflow (
release.testnet.yml) that drives the release end to end: preflight checks, version-bump PRs for doublezero and infra, a human-approved gate before pushing the 9 component tags, CloudSmith package verification, Solana program build and staging with a manual deploy gate, onchain version verification, infra core/client deploys, QA, and Slack notifications. Supportsdry_runfor plumbing validation and safe re-runs (existing PRs are reused; already-pushed tags are skipped via a newskip_existinginput on the tag workflow). Runbook atdocs/testnet-release.md. - Thread all testnet release orchestrator Slack posts under a single per-run parent message in
#bots, posted via the Slack Web API (chat.postMessagewith a bot token) instead of incoming webhooks, which cannot start threads. Adds a threaded PR-links post afteropen-prscovering the merge-both-PRs / approve-gate-1 human steps, and a tag-approval nudge aftergate-tagsfor the tag jobs' secondtestnetenvironment prompt. Slack failures degrade to workflow warnings and flat posts, never failing the release. (#4036) - Testnet release: the generated
DEPLOY.mdnow embeds the exact program-deploy commands (keypairs under~/testnet-ops/, artifacts from the staged release directory,doublezero initto refresh the onchain version) and links the infra deploy runbook, which replaced the Notion doc. Runbook recovery guidance updated to prefer "Re-run all jobs" after mid-pipeline failures. - Fix dry runs dead-ending after
stage-programs: the job-level skip ofpush-tagstransitively skipped every downstream default-condition job (a skipped ancestor poisonssuccess()for the whole graph, pastverify-cloudsmith's own override), sogate-programsthroughannouncenever ran in dry-run mode. The tag workflow gains adry_runinput and the tag jobs now run as validated no-ops instead of skipping — which also means dry runs exercise thetestnettag-approval prompt — andverify-cloudsmith's special-case condition is deleted.
- Auto-publish the mainnet-beta client base image daily when the stable Cloudsmith channel advances (idempotency-gated so a run with no new version is a no-op), and notify
- Dependencies
- Pin the workspace
solana-system-interfacerequirement to"3"andsolana-loader-v3-interfaceto"6"(were multi-major ranges>=1,<=3and>=5,<=6). The wide ranges letcargo update --workspacesilently rebind our crates' edges onto the older major the agave tree also pulls in, making lock resolution non-deterministic (poisoned the v0.30.0 release bump). No resolved versions change. (#4043)
- Pin the workspace
- RFCs
- Add RFC-26 proposing a pure, RPC-free Rust instruction-builder library (
doublezero_serviceability_instruction) with one SPL-style builder per serviceability instruction, decoupling instruction construction from signing/sending and centralizing account-order conventions.
- Add RFC-26 proposing a pure, RPC-free Rust instruction-builder library (
v0.29.0 - 2026-07-02
- SDK
- revdist Python SDK migrated to the async solana-py RPC API (solana-py 0.40.0 removed the sync
Client). TheClientread methods (fetch_config,fetch_distribution, etc.) are now coroutines and must be awaited;new_rpc_clientreturns anAsyncClient. (#3945)
- revdist Python SDK migrated to the async solana-py RPC API (solana-py 0.40.0 removed the sync
- Dependencies
- Migrate the entire Rust workspace from solana-sdk 2.3.x to the solana 3.0 line plus the granular split crates (
solana-pubkey,solana-instruction,solana-cpi,solana-sdk-ids,solana-system-interface,solana-commitment-config,solana-compute-budget-interface), aligning with the doublezero-solana programs. Onchain account layouts are unchanged (regenerated fixtures are byte-identical), so the Go, TypeScript, and Python SDKs are unaffected. (#3830)
- Migrate the entire Rust workspace from solana-sdk 2.3.x to the solana 3.0 line plus the granular split crates (
- Onchain programs
- Adapt to the solana 3.0 APIs:
AccountInfo::reallocbecomesresize, system-program and BPF-upgradeable-loader IDs move tosolana-sdk-ids,ProgramError::BorshIoErroris now a unit variant, andAccountInfo::newdrops itsrent_epochargument. Bump the programs build toolchain to Rust 1.91.
- Adapt to the solana 3.0 APIs:
- Client
- Add a
-route-liveness-backoff-maxdaemon flag to cap the Down-state liveness probe interval. Defaults to 60s (production behavior unchanged); the e2e harness pins a small value to avoid a probe gap that flaked the multi-client IBRL tests. (#3949) - Add a structured
subscriptionsarray todoublezero status(aftermulticast_groups) with per-group detail — group pubkey, code, multicast IP, max bandwidth, andpublisher/subscriberbooleans — so consumers no longer have to parse the flattenedP:/S:string. (#3964) - Originate a PIM Register beacon for multicast publishers:
doublezerodperiodically sends a PIM Register (encapsulating the publisher heartbeat) unicast to the RP over the tunnel, so the device originates the MSDP SA for the published source even on a dual-role publisher/subscriber tunnel, wherepim ipv4 border-routersource injection is suppressed by the subscriber-side PIM neighbor. (RFC-22)
- Add a
- CI
- Install agave v3.0.4 and build/test the SBF programs with platform-tools v1.54 (
SBF_TOOLS_VERSION), required because the solana 3.0 dependency tree pulls edition2024 crates that need Cargo >= 1.85 (agave's default platform-tools v1.51 ships Cargo 1.84.1).
- Install agave v3.0.4 and build/test the SBF programs with platform-tools v1.54 (
- Controller
- Permit the unicast PIM Register to the RP (
permit pim any host 10.0.0.0) on publisher multicast tunnels so the client-originated Register reaches the device;pim ipv4 border-routeris retained as a backstop. (RFC-22)
- Permit the unicast PIM Register to the RP (
- E2E tests
- Bump the e2e base image to agave v3.0.4 and build the onchain programs with platform-tools v1.54 to match the solana 3.0 migration.
- Pin the e2e ledger
solana-test-validatorto the deploy floor (agave 2.2.16, testnet) so a green e2e proves a change actually deploys and runs on the production cluster runtime. Previously the runtime validator rode the SBF build toolchain version (2.3.13); it is now decoupled and pinned independently. The build toolchain is unchanged. (#3957) - Fix a
TestE2E_Multicastflake where the post-connectdoublezero statuscheck could observe only the first multicast group. After incrementally adding the second group, the test relied onWaitForTunnelUp, which returns immediately because the first tunnel is already up, so the single-shot status assertion could race the onchain propagation and the daemon's cached program data. Add anEventuallypoll ondoublezero user listfor both groups before the post-connect checks.
- Smartcontract (Serviceability)
- Honor a Permission account bearing
ACCESS_PASS_ADMIN/USER_ADMINon theUpdateMulticastGroupRolesgrant path and theCreateSubscribeUserowner-override, so the feed oracle can subscribe validator-owned users and provision cross-owner users on a Permission account instead offoundation_allowlistmembership. Granting multicast roles requiresACCESS_PASS_ADMIN; removal-only cleanup staysUSER_ADMIN. Both handlers disambiguate the optional trailing Permission account from the EdgeSeat feed/device accounts by PDA match. The change is additive: every existing caller keeps its current authority. (#3966)
- Honor a Permission account bearing
v0.28.0 - 2026-06-26
- CLI
- Remove the
doublezero-adminbinary. Its commands now live in thedoublezeroCLI as hidden subcommands (e.g.doublezero sentinel ...,doublezero migrate flex-algo).
- Remove the
- Client
- Add a
--no-waitflag todoublezero disconnectthat skips waiting for the daemon to tear down the tunnel(s), exiting once the onchain user deletion is confirmed. (#3911)
- Add a
- CLI
doublezero user subscribecan now remove multicast roles:--publisher/--subscriberaccept an explicit value (--publisher false/--subscriber false) to drop a role, an omitted flag preserves the user's current role for the group, and the command errors when neither flag is given. Bare--publisher/--subscriberstill meantrue. (#3914)- Add hidden
migrate flex-algo(RFC-18 link-topology and Vpnv4 loopback FlexAlgoNodeSegment backfill); the priormigratecommand is nowmigrate user-pda. Moved fromdoublezero-admin. - Add hidden
device migrate-multicast-countsanddevice migrate-unicast-countsto reconcile stale per-device subscriber, publisher, and unicast-user counts. Moved fromdoublezero-admin. - Add hidden
sentinel find-validator-multicast-publishersandsentinel create-validator-multicast-publisherscommands. Moved fromdoublezero-admin. - Feature-gate
doublezero-sentinel's server-mode deps (Prometheus exporter) behind a default-onserverfeature and depend on it withdefault-features = false, so thedoublezerobinary no longer linksrustls/aws-lc-sys. Restores the glibc floor (binaries built on Ubuntu 24.04 load on 22.04 again) and shrinks the binary. - Add a
--narrowflag todevice list,link list, andaccess-pass listthat renders a width-reduced table for wide output — dropping low-value columns, abbreviating pubkeys to a copyable leading-prefix, and shortening headers — while leaving--jsonand the default table unchanged. (#3938)
- Onchain programs
- Validate the device
mgmt_vrffield against the account-code charset ([A-Za-z0-9:_-]) and a 32-byte length cap, matching the devicecodefield. Empty (the default VRF) is still accepted. - Transfer connect/disconnect credits to the user's account when adding a user to a multicast group's publisher or subscriber allowlist, so the user can connect immediately. The airdrop is atomic with the allowlist update and mirrors
set_access_pass(scaled forallow_multiple_ippasses). (#3851)
- Validate the device
- SDK
- Pass the
user_payeraccount on the multicast allowlist add instructions so the onchain credit transfer can fund it.
- Pass the
- Controller
- Skip rendering device config when a string field would not survive as a single config token (contains control or whitespace characters).
- Prune a device's per-pubkey Prometheus series when it is removed from the on-chain ledger, so the
Network: Device Stopped Calling Controlleralert auto-resolves instead of firing forever on a frozen counter. Check-ins from ledger-absent pubkeys are rejected, counted on the new aggregatecontroller_grpc_getconfig_unknown_pubkey_total, and logged at WARN (rate-limited). Registercontroller_link_metrics/controller_link_metrics_invalid_total(previously populated but never exposed); on each cache update delete only thecontroller_link_metricsgauge series that have gone stale (active last cycle, absent now — covering inactive links, removed/renamed interfaces, code changes, and devices that gain a pathology) while leaving still-active series in place so scrapes never see a gap; and prunecontroller_link_metrics_invalid_totalby device code when a device is removed from the ledger. (#3931)
- Device agents
- Reduce agent CPU usage by continuing to fetch the full config every 5 seconds but only applying when it has changed or after 60s timeout
- E2E tests
- Route all devnet networks (CYOA, default, and miscellaneous) through a shared collision-safe subnet allocator to prevent overlapping subnet assignments across concurrent test runs. (#3919)
- Harden the mainnet-beta QA client against flaky/stale Solana RPC: multi-endpoint failover (with a public-RPC default fallback when
SOLANA_RPC_FALLBACK_URLSis unset), active slot-lag detection, poll-until-consistent post-write reads, and configurable timeout/retry budgets. Eliminates manualSOLANA_RPC_URLrepointing during RPC outages. (#3930) - Make device selection deterministic in the maxusers rollover test by waiting until the nearby device is measured faster than the faraway device by more than the client's 5ms latency tolerance, so the client cannot connect to the wrong device on a tie. (#3936)
- Match CLI validation errors case-insensitively in the interface validation test, decoupling assertions from the program's error-message casing. (#3936)
v0.27.1 - 2026-06-10
- Client
- Revert auto-enabling allocated-IP mode on
doublezero connect ibrlbehind NAT (#3861): the RFC1918 heuristic misfires on 1:1 NAT hosts where plain IBRL works, silently changing the user type.
- Revert auto-enabling allocated-IP mode on
v0.27.0 - 2026-06-10
- Client
- Auto-enable allocated-IP mode for
doublezero connect ibrlwhen the daemon detects a private RFC1918 default-route source (behind NAT), unless-aor--client-ipis set. doublezero connect multicastwith no groups now auto-joins every group authorized in the caller's AccessPass — publishing tomgroup_pub_allowlistand subscribing tomgroup_sub_allowlist. An AccessPass with no authorized groups is a no-op.
- Auto-enable allocated-IP mode for
- Onchain programs
- Add per-category seat caps to
EdgeSeataccess passes (errors 89/90 on overflow), scale theSetAccessPassairdrop by the cap sum whenallow_multiple_ipis set, and drop the dynamic-pass IP-lock andIS_DYNAMICflag. (#3859)
- Add per-category seat caps to
- CLI
access-pass setgains--max-unicast-users/--max-multicast-users;get/listshow the per-category counts and caps.
- SDK
- Decode the four new
AccessPasscap fields (and the previously-missingtenant_allowlist) in the Go, Python, and TypeScript layouts. GetAccessPassCommandand the multicast allowlist resolver both resolve a shared dynamic-seat AccessPass (theUNSPECIFIEDPDA) before the exact-IP pass, matching the onchaincreate_userlookup. (#3853)
- Decode the four new
v0.26.0 - 2026-06-05
- Onchain programs
- Deprecate the
AccessPassStatus::Expiredaccess-pass status (renamedExpiredDeprecated; discriminant3retained for wire compatibility). Access-pass epoch expiry no longer demotes users toOutOfCredits:update_statusstops producing the status, and bothtry_activate(user creation) andCheckUserAccessPass(periodic re-check) keep usersActivated. Epoch validity is still enforced at user creation for unicast users only; multicast publishers and subscribers are governed bymgroup_*_allowlist, not by epoch.
- Deprecate the
- SDK
- Mirror the access-pass status rename in the Go, Python, and TypeScript deserializers:
AccessPassStatusExpiredDeprecated(Go),EXPIRED_DEPRECATED(Python), and the"expired (deprecated)"string across all three.
- Mirror the access-pass status rename in the Go, Python, and TypeScript deserializers:
- SDK (Rust)
- Remove the client-side
User not activeprecheck from the multicast subscribe/publish command (UpdateMulticastGroupRoles) so non-Activatedusers are no longer blocked before submission; authorization is enforced onchain.
- Remove the client-side
- CLI
- Extract
doublezero-daemon-clicrate housing theDaemonClienttrait and theenable,disable, andstatusdaemon verbs. The new crate owns all daemon HTTP interaction (Unix-socket client, response types, shared output helpers) and is consumed by thedoublezerobinary.check_daemonbindsget_environment()once per invocation instead of calling it per-check. - Fold
version,account,accounts,log, andsubscribediagnostic verbs from the binary's top-levelCommandenum intoServiceabilityCommandper RFC-20. Each verb now takes&CliContext+ generic&C: CliCommand+&mut Wwriter and is async. Add--jsontoaccount,accounts, andlog(RFC-20 §Output). The binary-levelsubscribeoverride uses the real blockingDZClient::subscribefor live event streaming; the module crate's implementation falls back to aget_all()snapshot for testability. - Change
geolocation user update-paymenttoupdate-payment-statusfor clarity. - geolocation
user get: Show probe code, rather than probe pubkey in target list. - geolocation
probe get: Show exchange code, rather than exchange pubkeys. - Remove
env/argvreads andeprintln!from the serviceability CLI module per RFC-20 §67. The keypair-source pre-flight check moves from a standalonehas_keypair_source()(which readstd::env::argsandstdin) to aCliCommand::has_keypair_sourcemethod computed once by the binary at startup; diagnostic output incheck_id,check_balance,check_allowlist, andprint_errornow routes throughtracing::error!instead of writing to stderr directly. - Add
--jsonoutput toglobalconfig feature-flags getandaccesspass user-balancesper RFC-20 §Output.feature-flags getnow renders a two-column (flags,raw) table by default instead of the priorEnabled feature flags: <names> (raw: <N>)/No feature flags enabled (raw: <N>)sentence.
- Extract
- CI
- e2e: report trusted fork e2e/shreds shard results onto the PR head SHA so branch protection's required
e2e (shard N)/shard-e2e (shard N)checks are satisfied by a/run-e2edispatch, removing the need for a maintainer to bypass the ruleset to merge fork PRs; also make the dispatcher's confirmation comment non-fatal so a cappedGITHUB_TOKENno longer fails the job after the runs have already launched (follow-up to #3777)
- e2e: report trusted fork e2e/shreds shard results onto the PR head SHA so branch protection's required
- Tools
tools/stress/device-reporter: surface device CPU + memory + agent RSS in the post-run summary. Thesummarysubcommand now reads the observer's per-tickshow processes top onceJSON captures andobserver.agent_metrics.jsonto render a## Resource usagesection: device CPU peak / p95 / sustained ≥ 80 % windows (matching the observer'scpu_sustainedabort threshold), memory peak free / used / floor-violation count, and doublezero-agent resident-memory peak / end / per-minute slope. New-free-mem-floor-mbflag (default1024, set0to disable) (#3845)- Complete the device-stress orchestrator (part 3): replace the stubbed agent runner with an SSH-backed runner that execs
doublezero-agent -verboseon the DUT and tees its output toorchestrator.agent.log, and a log parser that turns the agent's commit-diff lines intopre_commit_log/appliedrunlog events. Adds--dut-ssh-userand--no-agentflags. - Add
tools/stress/device-observer/, a per-device sampling tool that analyzes the output of the device-stress orchestrator and observer
- Telemetry
- Drop the redundant
ip-msdp-sa-cachekind from the state-ingest server's default state-collect command list.show ip msdp sa-cache rejectedalready returns the full SA cache (accepted SAs in theacceptedSaMsgarray plus any rejected SAs inrejectedSaMsg), so the bareshow ip msdp sa-cachecollection is redundant — devices were running both commands per tick and uploading the same accepted-SA data twice. Theip-msdp-sa-cache-rejectedkind is retained.
- Drop the redundant
- Telemetry (geoprobe)
- Retry transient
bind: invalid argumentfailures when allocating per-probe UDP sockets inPublisher.AddProbe, matching the existing retry-on-bind pattern inPinger. The shared retry helper is lifted intoretry.goso the publisher and pinger paths use the same exponential-backoff logic. Fixes intermittentTestPublisher_RemoveProbe/TestPublisher_AddProbeCI flakes caused by concurrent ephemeral-port allocation (#3765)
- Retry transient
- Makefile
- Add
unreadable_literalto make cargo clippy alert on large numbers written without_.
- Add
v0.25.1 - 2026-06-01
- SDK (Go)
- Add CreateUser / DeleteUser to the serviceability executor with cross-language wire-format fixtures and four new PDA helpers (GetUserPDA, GetAccessPassPDA, GetTunnelIdsPDA, GetDzPrefixBlockPDA)
- SDK (Rust)
- Add
DZClient::from_contextandGeoClient::from_context, which build clients directly from a resolved RFC-20CliContextinstead of re-reading~/.config/doublezero/cli/config.ymland re-applying moniker conversion. The context already carries the fully resolved ledger RPC/WS URLs and program IDs, so these constructors consume them verbatim, making the context the single source of truth and removing the double-resolution the binary previously incurred. Keypair precedence is preserved exactly (CLI flag >DOUBLEZERO_KEYPAIR> stdin > context keypair path > default): the raw--keypairflag is passed as the highest-precedence source and the context keypair path is used only as the low-precedence fallback, so the env var still wins. The new constructors and theirdoublezero-cli-coredependency are gated behind acli-contextcargo feature so non-CLI SDK consumers (controlplane, telemetry, e2e) keep a dependency-light default build.DZClient::new/GeoClient::neware unchanged for callers that do not build aCliContext(e.g.controlplane/doublezero-admin). - Drop the pre-submit
simulate_transactioncall inDZClient::execute_transaction_innerand submit withskip_preflight: true, eliminating the redundant double-simulation (the explicit simulate plussend_and_confirm_transaction's default preflight) on the happy path. Program logs are now recovered fromget_transactionon the failure path soSimulationError/SimulationTransactionErrorandDoubleZeroErrormapping in CLI output are unchanged. Trade-off: failing transactions now land onchain and burn fees instead of failing for free at simulation (#3750)
- Add
- CLI
- Honor the build-configured default environment (
Testnetby default,MainnetBetaunder thedefault-mainnet-betafeature) when neither--envnor a persistedconfig.ymlselects one. The RFC-20 context-build previously fell back toEnvironment::default(), which is alwaysDevnetregardless of the build, so a testnet build with no config silently targeted Devnet's ledger URLs and program IDs. The binary now resolves the fallback through the newdoublezero_sdk::default_environment(), matching the legacyDZClient::newdefaults (default_program_id,ClientConfig::default) which already key off the compiled-in environment (#3810) - Construct the serviceability and geolocation SDK clients in the
doublezerobinary viaDZClient::from_context/GeoClient::from_context, replacing the legacyDZClient::new(Option<String>, ...)bridge. The binary no longer round-trips the already-resolvedCliContextvalues back through the SDK's config-file re-resolution. No user-facing command, flag, or output change. - Restore environment-moniker support for the
--program-idand--geo-program-idglobal flags. The context-build resolved both flags with a rawparse::<Pubkey>(), so a moniker (e.g.--geo-program-id testnet) failed to parse, was silently dropped, and the binary fell back to the environment default. Both flags now accept monikers in their full (mainnet-beta,testnet,devnet,local) and short (m,t,d,l) forms, resolving to the matching program ID; a literal pubkey still passes through. A value that is neither a known moniker nor a valid pubkey is now a hard error instead of being silently ignored.convert_program_monikeris broadened to cover all four environments (previously onlydevnet/testnet), matchingconvert_geo_program_moniker. - Treat
--envas a base that the per-field flags override, rather than being mutually exclusive with them.--env <name>resolves the whole network (ledger URL, WS URL, Solana L1 URL, serviceability and geolocation program IDs);--url,--ws,--solana-url,--program-id, and--geo-program-ideach override only their own value on top (precedence: explicit flag >--env, per RFC-20 §override hierarchy). Applies to both the globaldoublezeroflags anddoublezero config set. Previously the global flag rejected the combination at the clap layer (ArgumentConflict) andconfig setprintedInvalid flag combinationand exited without writing, so--env local --program-id <X>was not possible. - Add
--solana-url <SOLANA_RPC_URL>global flag todoublezeroper RFC-20 §Global flags. Distinct from--url, which continues to override the DZ ledger transport;--solana-urltargets the Solana L1 transport. The flag is parsed and exposed on the binary'sAppstruct; per-verb consumption lands when verbs migrate to construct typed Solana L1 clients fromCliContext. - Add
--log-level <LEVEL>global flag and initialize thetracingsubscriber at startup.LEVELis one ofoff,error,warn(default),info,debug,trace. Diagnostic logs go to stderr so--jsonoutput on stdout remains parseable. Honors theRUST_LOGenvironment variable when set, overriding the CLI-flag level for per-module filtering. Replaces the previousprintln!("using keypair: ...")stdout line with atracing::info!event; the keypair confirmation now appears only at--log-level infoor higher and no longer pollutes parseable stdout. (Named--log-levelrather than the RFC-20 §Global-flags suggested--verbose/-vbecause the existingdoublezero connect/disconnectsubcommands already own a--verboseflag withbooltype; the global flag deviation will be revisited when the daemon-control module crate is carved out.) - Build a
CliContextonce at binary startup from--env, the per-field global overrides (--url,--ws,--solana-url,--program-id,--geo-program-id,--keypair,--sock-file), and the persisted~/.config/doublezero/cli/config.yml(overridable viaDOUBLEZERO_CONFIG_FILE), per RFC-20 (§CliContext). Precedence (highest wins): CLI flag > persisted config > env-derived default. When--envis not set and the persisted config has a serviceability program ID, the environment is derived from that program ID viaEnvironment::from_program_id; otherwise the binary falls back toEnvironment::default(). The legacyDZClientis now constructed from the fully resolvedCliContextURL, WebSocket, and program-ID values directly, so verbs that migrate to readCliContextsee the same backend as the legacy bridge. Keypair resolution is intentionally left toDZClient::new's internalload_keypairprecedence (CLI--keypairflag >DOUBLEZERO_KEYPAIRenv var > stdin > persisted config) so theDOUBLEZERO_KEYPAIRenv var continues to override the persisted keypair path, as relied on by the e2e contributor-auth negative-authz suite. File reads happen only in the binary; module crates remain forbidden from touching the filesystem (RFC-20 §67). - Centralize top-level error rendering through
doublezero_cli_core::error::render_eyre. Replaces three ad-hoceprintln!("Error: {e}")sites inclient/doublezero/src/main.rs(env-parse failure, env-config resolution failure, top-level command failure) with a single helper that printsError: <head>followed by the full chain of causes on stderr. - Rename the
smartcontract/cli/crate fromdoublezero_clitodoublezero-serviceability-clito satisfy RFC-20's module-crate naming contract (doublezero-<module>-cliin kebab-case). The crate stays atsmartcontract/cli/; only the[package].nameand[lib].namechange (lib name isdoublezero_serviceability_clibecause Rust requires underscores in import paths). All in-tree consumers are updated:client/doublezero,client/doublezero-geolocation-cli,controlplane/doublezero-admin, and the workspaceCargo.toml. External operators who depend on the workspace crate by its old name (doublezero_cli) must update theirCargo.tomlandusestatements. No user-facing command, flag, or output change. - Migrate
location getto the RFC-20 conforming verb pattern as the project's reference.GetLocationCliCommand::executeis nowasync fn, takes&CliContextas its first non-self argument, and emits atracing::debug!event so-vsurfaces what the verb is doing. The verb's user-facing args, flags, table layout, and JSON schema are unchanged. The unit test consumes the shareddoublezero_cli_core::testing::cli_context_default_for_tests()helper and continues to use the existingMockCliCommand(auto-generated by#[automock]) as the backend. Binary dispatch arms inclient/doublezeroandcontrolplane/doublezero-adminare updated to.awaitthe new method; other location verbs (Create, Update, List, Delete) keep their current sync signatures and migrate opportunistically. - Add
docs/cli-standard.md, the contributor-facing summary of RFC-20 with thelocation getworked example and pointers to the shared validators, formatters, logging facade, and test helpers indoublezero-cli-core. - Update
CLAUDE.mdwith a CLI-standard section pointing at RFC-20, the contributor doc, and the reference verb so future contributors land on the standard quickly. - Move the per-resource serviceability subcommand wrapper files (
accesspass,config,contributor,device,exchange,globalconfig,link,location,multicastgroup,permission,resource,tenant,user) fromclient/doublezero/src/cli/into the module crate atsmartcontract/cli/src/cli/per RFC-20 §Module contract item 2. Internal imports in the moved files switch fromdoublezero_serviceability_cli::<resource>::*tocrate::<resource>::*. Binary import paths inclient/doublezero/src/{cli/command.rs,main.rs}switch todoublezero_serviceability_cli::cli::<resource>::*.cli/multicast.rsstays in the binary because itsSubscribe/Unsubscribe/Publish/Unpublishvariants are async and depend on binary-local daemon-control infrastructure (ServiceControllerImpl,crate::command::helpers::resolve_client_ip); the binary now importsMulticastGroupCliCommandfrom the library. - Introduce
doublezero_serviceability_cli::cli::ServiceabilityCommand, the module crate's top-level subcommand enum +async fn execute(ctx, client, out)dispatcher per RFC-20 §Module contract item 2. Aggregates 17 serviceability variants (Init,Migrate,Address,Balance,Config,GlobalConfig,Location,Exchange,Contributor,Permission,Tenant,Device,Link,AccessPass,User,Export,Keygen,Resource) and owns the full dispatch tree currently inlined inclient/doublezero/src/main.rs. Defined but not yet wired into the unified binary; the next PR adds#[command(flatten)] Serviceability(ServiceabilityCommand)to the binary'sCommandenum and collapsesmain.rsto a single dispatch arm. - Hoist
ServiceabilityCommandinto the unifieddoublezerobinary via#[command(flatten)]on the binary'sCommandenum. Drops 17 explicit variants and collapses themain.rsdispatch match block from roughly 270 lines to one arm (Command::Serviceability(cmd) => cmd.execute(&ctx, &client, &mut handle).await). The binary retains daemon-control verbs (Connect,Enable,Disable,Status,Disconnect,Latency,Routes), raw-DZClientdiagnostics (Account,Accounts,Log), the binary-local geolocation tree,InitGeolocationConfig, the multicast dispatch (whoseSubscribe/Unsubscribe/Publish/Unpublishasync arms depend on daemon-control infrastructure), and theCompletiongenerator. User-facingdoublezero --helpis byte-identical to the pre-refactor output (29 visible top-level commands); no flag, name, or output change. BinaryCommandenum andmain.rsdispatch are unchanged; this is pure file relocation. - Move
MulticastGroupCommandsdispatch out ofclient/doublezero/src/main.rsand into apub fn execute(&client, &mut out)method on the enum itself, defined next to the enum insmartcontract/cli/src/cli/multicastgroup.rs. Mirrors the per-resource dispatch pattern inServiceabilityCommand::executeand finishes the flatten/collapse work for the one module-crate subtree reached through the binary'sMulticastCliCommandwrapper (which has to stay binary-local because itsSubscribe/Unsubscribe/Publish/Unpublisharms depend onServiceControllerImpl). The binary'sMulticastarm shrinks from a 5-level nested match (Allowlist → Publisher/Subscriber → Add/Remove/List, plus Create/Update/List/Get/Delete at the Group level) toMulticastCommands::Group(args) => args.command.execute(&client, &mut handle). No flag, name, or user-facing output change. - Add cross-verb helpers in
doublezero-cli-coreto drop the per-verb boilerplate that every list/get/create/update/delete verb repeats today: therequire!macro (one-line readiness check that expands toclient.check_requirements(flags.bits())?so the legacyu8trait signature stays unchanged andMockCliCommandkeeps working),render_collection<T: Tabled + Serialize>andrender_record<T: Tabled + Serialize>(the--json/--json-compact/ table three-branch switch),print_signatureandprint_signature_and_then(theSignature: <sig>write-verb tail and its--waitcompanion), andOutputFormat::from_flags(json, json_compact)(resolves the per-verb boolean flags to the enum).tabledbecomes adoublezero-cli-coredependency so the rendering helpers can construct tables; module-crate verbs continue to importtabled::Tableddirectly for their per-verb display types. Helpers ship with unit-tested byte-identical output to the pre-refactor code paths. - Add
resolve_location_pk(client, pubkey_or_code)tosmartcontract/cli/src/helpers.rs, the per-resource pubkey-or-code resolver used bylocation updateandlocation delete. Centralizes the existingclient.get_location(GetLocationCommand { pubkey_or_code: ... })?pattern so the verb body no longer carries the lookup boilerplate; additional resolvers (resolve_device_pk,resolve_link_pk, ...) land per-resource as their sweep PRs migrate the other verbs. - Migrate all five
locationverbs (create,update,list,get,delete) to the RFC-20 conforming shape ahead of the per-resource sweeps. Every verb is nowpub async fn execute(self, ctx: &CliContext, client: &C, out: &mut W) -> eyre::Result<()>, consumes the new helpers (require!,render_collection,render_record,print_signature,resolve_location_pk), and forwards through the dispatcher with.await. Behavior is byte-identical: table layout, JSON schema,Signature: <sig>line, and the--json/--json-compactsemantics all match the pre-refactor output exactly (existing tests pass without assertion changes).controlplane/doublezero-admin'sLocationCommandsarm is updated to forward&ctxand await every verb. Other resources (exchange,contributor,tenant,device,link,user,multicastgroup,accesspass,globalconfig,permission,resource) continue to compile against their existing syncpub fn execute(self, client, out)signatures and migrate opportunistically in subsequent PRs. - Lift the
block_onasync-test helper intodoublezero_cli_core::testinginstead of redeclaring it verbatim in every async verb's test module (it was already copied five times across thelocationverbs). The helper is gated behind a newtestingcargo feature sotokiostays an optional dependency and the defaultdoublezero-cli-corebuild remains dependency-light; module crates enabledoublezero-cli-core = { workspace = true, features = ["testing"] }in their dev-dependencies. The fivelocationverb tests now importblock_onfrom the shared module. - Ship shell-completion scripts in the client installer and recommend
bash-completionso apt/dnf pull it in when available.build/is added to.gitignore. - Migrate all six
exchangeverbs (create,update,list,get,delete,set-device) to the RFC-20 conforming shape on top of the shared CLI helpers. Every verb is nowpub async fn execute(self, ctx: &CliContext, client: &C, out: &mut W) -> eyre::Result<()>, consumes the helpers (require!,render_collection,render_record,print_signature), and the update/delete/set-device paths route their pubkey-or-code argument through a newresolve_exchange_pkhelper insmartcontract/cli/src/helpers.rs. The pre-existing BGP community range check inexchange updateis preserved.exchange set-deviceretains its legacyOption<String>::and_thensemantics for--device1/--device2(an unknown device silently resolves toNone, which clears the slot) under an explanatory comment.controlplane/doublezero-admin, the unifieddoublezerobinary, and the serviceability dispatcher all forward&ctxand await every exchange arm. Behavior is byte-identical: table layout, JSON schema,Signature: <sig>line, and--json/--json-compactsemantics match pre-refactor output exactly; all 7 exchange unit tests pass without assertion changes. - Migrate all five
contributorverbs (create,update,list,get,delete) to the RFC-20 conforming shape on top of the shared CLI helpers. Every verb is nowpub async fn execute(self, ctx: &CliContext, client: &C, out: &mut W) -> eyre::Result<()>, consumes the helpers (require!,render_collection,render_record,print_signature), andupdate/deleteroute their pubkey-or-code argument through a newresolve_contributor_pkhelper insmartcontract/cli/src/helpers.rs. The duplicate-code precondition increateandupdateis preserved, as is theowner = "me"short-circuit increatethat resolves to the payer.update's pubkey resolution now goes through the shared helper rather than an in-linePubkey::from_str(the old code-by-pubkey path was a code-or-pubkey path despite the variable name; the resolver accepts both).controlplane/doublezero-admin, the unifieddoublezerobinary, and the serviceability dispatcher all forward&ctxand await every contributor arm. Behavior is byte-identical: table layout, JSON schema,Signature: <sig>line, and--json/--json-compactsemantics match pre-refactor output exactly; all 5 contributor unit tests pass without assertion changes. - Migrate the 5
multicastgroupCRUD verbs (create,update,list,get,delete), the 6multicastgroup allowlistverbs (publisher + subscriberadd/list/remove), the 6 standalone foundation/QAallowlistverbs (foundation add/list/remove,qa add/list/remove), the 8userverbs (create,create-subscribe,subscribe,request-ban,update,list,get,delete), and the 9globalconfigverbs and sub-tree verbs (get,set,set-version,airdrop get/set,authority get/set,feature-flags get/set) to the RFC-20pub async fn execute(self, ctx: &CliContext, client, out)signature. Signature-only sweep: verb bodies (including the post-write--waitpolling inuser create-subscribe/subscribeand the bespokemulticastgroup updatere-fetch flow) are unchanged.MulticastGroupCommands::executeitself flips from sync to async and propagatesctxthrough its nested allowlist arms; the binary'sMulticastarm becomesargs.command.execute(&ctx, &client, &mut handle).await(single line). Test files gain the per-fileblock_onshim andcli_context_default_for_tests()import.controlplane/doublezero-admin, the unifieddoublezerobinary, and the serviceability dispatcher all forward&ctxand await every multicastgroup, allowlist, user, and globalconfig arm. All 345 unit tests pass byte-identically. Helper adoption (require!,print_signature,render_collection,render_record) lands opportunistically in follow-up PRs. - Migrate the 11
deviceanddevice interfaceverbs (devicecreate,update,list,get,delete,set-healthplus interfacecreate,update,list,get,delete) and the 14linkandtopologyverbs (linkaccept,delete,wan create,dzx create,get,latency,list,set-health,updateplus topologyassign-node-segments,clear,create,delete,list) to the RFC-20pub async fn execute(self, ctx: &CliContext, client, out)signature. Signature-only sweep: verb bodies (including--waitpolling viapoll_for_*_activated, the per-verb requirement checks, and theSignature:writes) are unchanged. Test files gain a per-fileblock_onshim andcli_context_default_for_tests()import so the existing sync#[test]bodies can drive the now-asyncexecute.controlplane/doublezero-admin, the unifieddoublezerobinary, and the serviceability dispatcher all forward&ctxand await every device, interface, link, and topology arm. All 345 unit tests pass byte-identically (92 in the migrated modules: device 46, link 29, topology 17). Helper adoption (require!,print_signature,render_collection,render_record) lands opportunistically in follow-up PRs; the--waitpolling flow ondevice create/update,device interface create/update,link wan-create/dzx-create/accept/updateneeds special handling there since the post-signature poll has to be preserved. - Migrate all six
accesspassverbs (set,close,list,get,user-balances,fund) and all sixresourceverbs (allocate,create,deallocate,get,close,verify), plus the eight leaf single-file verbs (address,balance,init,migrate,keygen,export,config get,config set), to the RFC-20pub async fn execute(self, ctx: &CliContext, client, out) -> eyre::Result<()>signature. The five small leaf verbs (address,balance,init,migrate,keygen) also adopt therequire!macro and (where applicable) theprint_signaturehelper since their bodies were one-line readiness checks paired with a singleSignature:write. The larger and more idiosyncratic verbs (config get/setwhich manipulate the persisted YAML,exportwhich serializes the whole graph, the accesspass and resource verbs which contain bespoke output and progress-spinner logic) keep their existing bodies for now and only get the signature flip; helper adoption for those lands opportunistically in follow-up PRs.controlplane/doublezero-admin, the unifieddoublezerobinary, and the serviceability dispatcher all forward&ctxand await every accesspass, resource, and leaf-verb arm.config get/settests gain a per-fileblock_onshim and acli_context_default_for_tests()import so the existing sync#[test]bodies can still drive the now-asyncexecute. The bespokeaccesspass fundsignature (R: BufReadfor stdin) is preserved — only the_ctxparameter is inserted afterself. Behavior is byte-identical: table layouts, JSON schemas,Signature:lines, thefundinteractive flow, and theconfigtext output all match the pre-refactor strings exactly; all 345 unit tests pass without assertion changes. - Migrate all eight
tenantverbs (create,update,list,get,delete,administrator add,administrator remove,update-payment-status) and all sixpermissionverbs (set,suspend,resume,delete,get,list) to the RFC-20 conforming shape on top of the shared CLI helpers. Every verb is nowpub async fn execute(self, ctx: &CliContext, client: &C, out: &mut W) -> eyre::Result<()>, consumes the helpers (require!,render_collection,render_record,print_signature), and tenant verbs that accept a pubkey-or-code identifier (update,delete,add-administrator,remove-administrator,update-payment-status) route through a newresolve_tenant_pkhelper insmartcontract/cli/src/helpers.rs. The duplicate-code precondition intenant createis preserved, as is theadministrator = "me"short-circuit.tenant delete's bespoke two-line output ("✓ Tenant 'X' deleted successfully\n Signature: ..."), its cascade-delete progress spinners, and its reference-count polling loop are preserved with manualwriteln!calls and an explanatory comment.permission set's bespoke two-line aligned output ("Signature: ..." + "Permissions: ...") is preserved the same way. Permission verbs derive the on-chain PDA fromuser_payerrather than going through a pubkey-or-code resolver.controlplane/doublezero-admin, the unifieddoublezerobinary, and the serviceability dispatcher all forward&ctxand await every tenant and permission arm. Behavior is byte-identical: table layout, JSON schema,Signature:line shape, and--json/--json-compactsemantics match pre-refactor output exactly; all 18 tenant and 18 permission unit tests pass without assertion changes. - Migrate
doublezero geolocationsubcommands into the newdoublezero-geolocation-climodule crate per RFC-20. Theprobeandusersubtrees and the hiddeninitverb are now owned by the crate; the binary mounts them viaGeolocationArgsfromdoublezero-geolocation-cli. The hidden top-leveldoublezero init-geolocation-configalias is removed; usedoublezero geolocation initinstead. - Validate CYOA/DIA interfaces have non-zero
--bandwidthindoublezero device interface createanddoublezero device interface update, and validateinterface[a|z].bandwidth >= link.bandwidthindoublezero link wan-createanddoublezero link dzx-create(DZX checks side A only). Mirrors the new onchain enforcement so misconfigured commands fail before submitting a transaction. - Validate
interface[a|z].bandwidth >= link.bandwidthindoublezero link accept(DZX accept path) before submitting the transaction, with the same human-readable message style aslink wan-create/link dzx-create. Mirrors the new onchain check. - Restore the default value for
doublezero device interface create --bandwidthso it is optional again. #3077 droppeddefault_valuefrom the clap attribute onbandwidth; because the field isu64(notOption<u64>), clap then treated omission as a missing required argument, even though the PR description stated--bandwidthwas now optional (#3775) - Route the
doublezero user get"no Access Pass found" warning throughtracing::warn!instead ofeprintln!, so diagnostics go to the logging facade per RFC-20 §Diagnostic logging and no longer write directly to stderr. No user-facing output, flag, or schema change. - Reconcile the RFC-20 docs with the implemented global logging flag:
rfcs/rfc20-cli-standardization.mdanddocs/cli-standard.mdnow describe--log-level <off|error|warn|info|debug|trace>(defaultwarn) instead of the never-implemented repeatable--log-verbose. Documentation only; the binary is unchanged.
- Honor the build-configured default environment (
- Tools
- Add
tools/stress/device-orchestrator/, the device-stress orchestrator skeleton for the GRE Tunnel Capacity Study (part 2 of #3746). Runs a batched provision-then-reverse-deprovision sweep against a live serviceability program, dumpingorchestrator-config.jsonand emitting a JSONL runlog ofsubmit | confirm | activate | deprovision_*events. Cooperates with an abort sentinel file: finish the in-flight user, tear down everything created, exit non-zero. The SSH-backed agent runner (pre_commit_log/appliedevents) is stubbed behindpkg/agent.Runnerand lands in part 3 (#3771). - Add
tools/stress/device-observer/initial scaffolding plus eAPI device sampler that writes per-tick snapshots of fiveshowcommands and anobserver-config.jsonwith the observer PID; collectors for Prometheus scrape, log tailers, and abort decider are stubbed and will be replaced in follow-up PRs. - Implement the
tools/stress/device-observer/Prometheus scraper for the doublezero-agent metrics endpoint. Each tick fetches--agent-metrics-url, parses the exposition response, and appends one NDJSON row per metric sample toobserver.agent_metrics.json. Counter family totals are also exposed via a thread-safeScraper.Snapshot()for downstream consumers. Per-tick HTTP, parse, or write failures log at WARN and the loop continues.
- Add
- E2E/QA
- Configure the manager and client CLI with
doublezero config set --env local, keeping per-field overrides for the container-specific ledger URLs and the deployed serviceability program ID (and geolocation program ID on the manager). The override is required because the sentinel multicast-publisher test deploys serviceability at a generated keypair and the compatibility tests clone testnet/mainnet program IDs; for standard stacks the env defaults already match the fixed localnet pubkeys. Exercises the new--env-as-base behavior end-to-end. - add trusted fork PR e2e dispatch (#3777)
- e2e/qa: remove client-side capacity pre-filtering from
ValidDevices, because the QA user pubkey bypasses capacity limits using the serviceability global-config qa-allowlist. Individual device failures no longer fail the test; instead, overall and per-host failure rates are evaluated after all batches and the test only fails if either exceeds--failure-threshold(default 10%) or--per-host-failure-threshold(default 20%).
- Configure the manager and client CLI with
- Controller
- Enable eos-native gnmi provider (#3781)
- Smartcontract (Serviceability)
- Enforce non-zero bandwidth on CYOA/DIA interfaces in
process_create_device_interfaceandprocess_update_device_interface. On update the rule fires only when the transaction is changing CYOA, DIA, or bandwidth, so legacy zero-bandwidth CYOA/DIA interfaces already onchain can still be updated for unrelated fields without first being repaired. Enforceside_a_iface.bandwidth >= link.bandwidth(andside_z_iface.bandwidth >= link.bandwidthfor WAN; DZX side Z is external) inprocess_create_link. Enforce the same rule for both side A and side Z inprocess_accept_link(the DZX accept path), so DZX side Z's bandwidth is validated when it is first bound and side A is re-validated in case it was lowered viaprocess_update_device_interfacebetween create and accept. All rejections surface asDoubleZeroError::InvalidBandwidth(Custom(31)).
- Enforce non-zero bandwidth on CYOA/DIA interfaces in
- Telemetry
- Replace the geoprobe
MinCachebest/backup eviction with a guarded-backup pattern: a backup is only collected whilebestis within its finalmaxAge/2("guard") window, so onbest's expiry the promoted value is always a recent-window minimum rather than a stale fallback. A new record low resetsbestand clearsbackup, and expiry now promotes in a loop so a backup that is itself already expired cannot be promoted.Best/BestRttNsbecome pure read-through accessors returning the lower of the two non-expired slots without mutating or promoting. - Add
ip-mroute,ip-mroute-count, and the four MSDP show-commands (ip-msdp-summary,ip-msdp-pim-sa-cache,ip-msdp-sa-cache,ip-msdp-sa-cache-rejected) to the state-ingest server's default state-collect command list. Devices with--state-collect-enablewill run each command via Arista eAPI and upload signed JSON snapshots to S3; downstream parsing into ClickHouse lands in separatelake/indexer/pkg/dzingestPRs (one per kind family).
- Replace the geoprobe
v0.24.0 - 2026-05-22
- Smartcontract
- Deprecate the 13 contributor-side program instructions whose only client was the now-deleted activator:
ActivateDevice(21),RejectDevice(22),CloseAccountDevice(27),ActivateLink(29),RejectLink(30),CloseAccountLink(35),ActivateMulticastGroup(47),RejectMulticastGroup(48),DeactivateMulticastGroup(53),ActivateDeviceInterface(72),RemoveDeviceInterface(75),UnlinkDeviceInterface(77), andRejectDeviceInterface(78). Dispatch arms now short-circuit toDoubleZeroError::Deprecated(custom code 67); processor files and argument structs are removed. Borsh variant tags are preserved (unit variants) so the wire format is unchanged — old clients receive a deterministic deprecation error rather than an unknown-instruction decode failure. BumpsMIN_COMPATIBLE_VERSIONto0.15.0(theclient/v0.14.1git tag was a patch release built from a commit whose workspace Cargo version was still0.14.0, so the v0.14.1 binary self-reports as 0.14.0 in its startup version check; v0.15.0 is the first release whose embedded version actually satisfies the intended ≥ 0.14.1 gate). Gated on onchainProgramConfig.min_compatible_version ≥ 0.15.0(#3623) - Deprecate the
ActivateUser,RejectUser,CloseAccountUser, andBanUseruser-lifecycle program instructions: dispatch arms now returnDoubleZeroError::Deprecated(custom code 67), and the processor files / argument structs are removed. Borsh variant tags 37/38/43/45 are preserved so the wire format is unchanged. The activator was the only client of all four —CreateUserhas been atomic-to-Activatedsince RFC-11,closeaccountwas activator-driven only, andRequestBanUseris now atomic. Gated on onchainmin_compatible_version ≥ 0.12.0(#3622) - Extend
SetUserBGPStatuswithbgp_rtt_ns: u64(smoothed BGP TCP RTT in nanoseconds, same unit asLink.delay_ns/Link.jitter_ns); appendbgp_rtt_nsto theUseraccount. Old payloads (status-only) decode withbgp_rtt_ns = 0viaBorshDeserializeIncremental; old serialized accounts decode withbgp_rtt_ns = 0via the existing append-only field pattern. Deploy order is unconstrained.
- Deprecate the 13 contributor-side program instructions whose only client was the now-deleted activator:
- SDK (Rust)
- Delete the now-dead
{Activate,Reject,CloseAccount}DeviceCommand,{Activate,Reject,CloseAccount}LinkCommand,{Activate,Reject,Deactivate}MulticastGroupCommand, and{Activate,Reject,Remove,Unlink}DeviceInterfaceCommandwrappers and the corresponding orphaned trait methods onDoubleZeroProgram— none are reachable from any live caller (#3623) - Delete the now-dead
ActivateUserCommand,RejectUserCommand,CloseAccountUserCommand, andBanUserCommandwrappers — none are reachable from any live caller.RequestBanUserCommand(operator-driven, atomic) is unaffected (#3622) - Unconditionally request the protocol maximum compute (1,400,000 CU) and heap frame (256 KiB) on every serviceability transaction sent through
DZClient. RemovesDoubleZeroClient::execute_transaction_with_compute_unit_limitand the per-instructionSET_GLOBAL_CONFIG_COMPUTE_UNIT_LIMIT/ASSIGN_TOPOLOGY_NODE_SEGMENTS_COMPUTE_UNIT_LIMITconstants — serviceability runs on a dedicated private Solana cluster, so raising every transaction to the protocol max is free (#3742)
- Delete the now-dead
- SDK (Go/TS/Python)
- Add the
bgp_rtt_nsfield to theUserdeserializer in all three SDKs; the Go executor builder accepts and serializes the new field via a 10-byte instruction payload.
- Add the
- Telemetry
bgpstatusnow discovers VRFs by enumerating/var/run/netns/instead of deriving namespace names from onchain tenant data and a hardcoded base prefix. Fixes multicast user BGP status never being collected on Arista EOS: the previous code assumed the default VRF was the agent's current Linux namespace, but on the device the default VRF is exposed as/var/run/netns/defaultwhile the agent runs inns-management. Drops the--bgp-namespaceflag wiring from the BGP status submitter (the flag is still used by the state collector); addsNetnsDirconfig (default/var/run/netns); removes the empty-string short-circuit innetns.RunInNamespace.bgpstatusreports per-user BGP RTT onchain on every status-change or periodic-refresh write. RTT comes from the same INET_DIAG snapshot already used to detect ESTABLISHED sessions (state.BGPSocketState.RTTms × 1_000_000to ns), so no new collection cost. RTT does not by itself trigger a submission; it piggybacks on writes that would already happen, capped byPeriodicRefreshIntervalstaleness. A Down submission always carriesbgp_rtt_ns = 0to avoid stale RTT outliving a missing session.- gnmi-writer now collects ISIS global state (
isis_global_state: instance, NET, level capability per network instance) and the ISIS LSP overload bit (isis_overload_bit) into ClickHouse, with companion*_latestviews; moves another slice of ISIS telemetry off the S3 pipeline and onto gnmi-writer so we can eventually retire the S3 ISIS path - gnmi-writer now stores per-interface unicast/multicast/broadcast packet counters (in/out) and FCS errors in the
interface_statetable, bringing it closer to parity with the interface counters already collected in InfluxDB. These already stream in the gNMI interface-state subtree, so there is no added collection cost; existinginterface_staterows read back0for the new columns - Remove the InfluxDB writer from global-monitor; ClickHouse is now the sole telemetry backend, dropping the
INFLUX_*env var configuration - Signed-TWAMP reflector in
geoprobe-agentissues a per-pair 8-byte challenge nonce inReply0.SinceLastRxNsand flagsReply1.NumOffsetsbit 7 (newChallengedfield onReplyPacket) whenProbe1.Sec || Fracechoes the nonce — proves the sender received Reply 0 before sending Probe 1, closing the pre-emit-Probe-1 attack onSinceLastRxNs. Backwards-compatible: legacy senders never echo the nonce so the flag bit stays 0, and they always ignored Reply 0's previously-zeroSinceLastRxNs. Documented in RFC16's new "Challenge-Response Inbound Probing" subsection (#3737) geoprobe-target-sendergains opt-in--challengedflag (default off). When set, the sender extracts the nonce fromReply0.SinceLastRxNs, writes it intoProbe1.Sec || Frac, signs Probe 1 only after Reply 0 is parsed, and surfacesReply1.Challengedon every per-pair log line (JSON"challenged", textChallenged Inbound:). Default off preserves the existing pre-sign-both / fire-Probe-1-immediately fast path byte-for-byte. Trade-off: challenged mode inflatesReply1.SinceLastRxNsby the sender's Probe 1 signing latency (#3738)- state-ingest no longer logs a spurious
server exited with error: use of closed network connectionat shutdown; the listener-closed race during graceful shutdown (net.ErrClosed) is now treated as a clean stop alongsidehttp.ErrServerClosed
- CLI
- Introduce
doublezero-cli-core(crates/doublezero-cli-core/), the shared library crate that everydoublezero-<module>-cliwill reuse per RFC-20. ShipsCliContext+CliContextBuilder(resolved configuration value carried into every verb),RequirementCheckbitflags aligned with the legacyCHECK_ID_JSON | CHECK_BALANCE | CHECK_FOUNDATION_ALLOWLISTbit values, the shared validator set (validate_pubkey,validate_pubkey_or_code,validate_code,validate_parse_bandwidth,validate_parse_delay_ms,validate_parse_jitter_ms,validate_parse_delay_override_ms), display formatters (DisplayVec,stringify_vec), atracing+tracing-subscriberinit_logging(verbosity)helper that writes to stderr, and atestingmodule with aCliContextbuilder for verb unit tests. Existing call sites insmartcontract/clicontinue to compile unchanged:smartcontract/cli/src/validators.rsandformatters.rsare now thinpub useshims over the core crate. - Add
solana_l1_rpc_urltodoublezero-config::NetworkConfig. Per RFC-20 §Environments:mainnet-betaresolves tohttps://api.mainnet-beta.solana.com,testnettohttps://api.testnet.solana.com,devnettohttps://api.testnet.solana.com(intentional asymmetry, see RFC), andlocaltohttp://localhost:8899. A newDZ_SOLANA_RPC_URLenvironment variable overrides the resolved value, mirroring the existingDZ_LEDGER_RPC_URL/DZ_LEDGER_WS_RPC_URLoverrides. - Drop the activator-only pollers from
doublezero(user and multicastgroup activation waits). The--waitflag onuser create,user create-subscribe,user subscribe,multicastgroup create, andmulticastgroup updatenow fetches the post-create state once instead of polling; creates are atomic toActivatedpost-RFC-11, so the wait loop was watching a transition that no longer happens (#3614) doublezero geolocationprobe ...anduser ...mirrorsdoublezero-geolocationversions; new--geo-program-idglobal flag,config get/setinclude Geolocation Program ID; new-init-geolocation-configfor init of geolocation program- cli:
doublezero geolocationprobe ...anduser ...mirrorsdoublezero-geolocationversions; new--geo-program-idglobal flag,config get/setinclude Geolocation Program ID. geolocation probe listnow includes signing pubkeys- Drop the activator-only pollers from
doublezero(user and multicastgroup activation waits). The--waitflag onuser create,user create-subscribe,user subscribe,multicastgroup create, andmulticastgroup updatenow fetches the post-create state once instead of polling — creates are atomic toActivatedpost-RFC-11, so the wait loop was watching a transition that no longer happens (#3614) - Trim the
Rejectedstatus arm from the device and link activation pollers;Rejectedwas itself an activator-driven transition (#3614) doublezero user getanddoublezero user listsurface BGP RTT as anrttcolumn (e.g.5.50 ms, or-when no sample has been observed). JSON output includes rawbgp_rtt_nsalongside the prettybgp_rttstring.- Remove standalone
doublezero-geolocationbinary; usedoublezero geolocation ...instead.
- Introduce
- Client
- Simplify
doublezero connect's post-create user fetch to a fixed retry-on-RPC-lag get instead of waiting forUserStatus::Activated; the activator-driven transition is gone, so the fetch only needs to ride out replica lag (#3614)
- Simplify
- E2E
- Switch geolocation invocations to
doublezero geolocation ...anddoublezero init-geolocation-config
- Switch geolocation invocations to
- Agent: log after Arista eapi commit
- Agent: log received config size in bytes and expose
doublezero_agent_config_size_in_linesanddoublezero_agent_config_size_in_bytesPrometheus gauges (#3741) - Controller
- Add
--max-user-tunnel-slotsflag to override the per-device user-tunnel slot count of 128 at runtime.
- Add
v0.23.0 - 2026-05-15
- Smartcontract
- Tenant administrators can now create or update access passes scoped to their tenant without being added to the foundation allowlist; non-privileged callers cannot remove a tenant they do not administer from an existing access pass.
- Skip
last_access_epochenforcement forUserType::MulticastinCreateSubscribeUserandCheckUserAccessPass. Multicast access is gated bymgroup_pub_allowlist/mgroup_sub_allowliston the access pass, not by epoch, so multicast users can be created and remainActivatedregardless of the access-pass expiry. IBRL/unicast epoch enforcement is unchanged.
- Client
- add
--tenantflag todoublezero user updatefor foundation-driven tenant reassignment - break latency ties with avg latency (#362)
doublezero connect multicastno longer fails the client-sidecheck_accesspassepoch check; only the AccessPass existence is verified for multicast. IBRL paths still enforcelast_access_epoch >= current_epoch.- Break latency ties using average latency when ranking candidate devices (#3692)
- Delete
InterfaceV3and theInterfaceDeprecated::V3variant from the serviceability program. V3 was added by an earlier change, never written to production accounts, and reverted in #3653 / no longer produced after #3667; this removes the dead type. Discriminant 3 is now an unused reserved slot inInterfaceDeprecated's encoding space — unknown discriminants fall through toInterfaceV2::default(). Removes the V3 struct, its helper impls (From<InterfaceV2>,TryFrom<&InterfaceV1>,Default,TryFrom<&InterfaceV3> for InterfaceV2), V3 match arms inInterfaceDeprecated::to_v2/size/Device::TryFrom, and the V3 cross-language byte-layout debug test. On-disk write format is unchanged (#3664) - break latency ties with avg latency (#362)
- add
- SDK
- Drop V3 handling from the Go, Python, and TypeScript serviceability readers: remove
DeserializeInterfaceV3(Go) and theversion === 3/version == 3legacy-slot branches (Python/TS); remove theTestDeserializeInterfaceV3CrossLanguageGo test. The forward-compat trailinginterfacesvec continues to carryflex_algo_node_segmentsvia the size-prefixed body — that path is unchanged (#3664) - Let side-Z contributors update a link's
status/desired_status/delay_override_nsviaUpdateLinkCommand; the Rust SDK now auto-detects the signer's side and builds the 4-account side-Z preamble the on-chain processor expects, instead of always sending the side-A layout (#3702)
- Drop V3 handling from the Go, Python, and TypeScript serviceability readers: remove
- Controller:
- Enforce interface MTU during config render from interface role (CYOA/DIA → 1500, fabric → 9000) instead of trusting onchain
Interface.Mtu/Link.Mtu; render each parent interface exactly once withmaxof its subinterface MTUs; change thetunnel.tmplfallback from2048to9000. Guards against stale V1 onchain interfaces (Mtu = 0) and duplicate parent blocks that previously caused silent IS-IS adjacency failures (#3696)
- Enforce interface MTU during config render from interface role (CYOA/DIA → 1500, fabric → 9000) instead of trusting onchain
- E2E tests
- Make multicast QA failures self-explanatory: require a
Multicast-typed status entry after multicast connect (instead of accepting a stale IBRL one), retryMulticastJoinbriefly on "interface not found" to absorb the daemon/kernel race, snapshot host state once after 30s of zero packets, and heartbeat the publisher's tunnel status during send windows to catch silent regressions
- Make multicast QA failures self-explanatory: require a
v0.22.0 - 2026-05-08
- Smartcontract
- Rename the
BackfillTopologyinstruction toAssignTopologyNodeSegmentsacross the program, CLI, and Rust SDK; the instruction discriminant (110) and on-disk semantics are unchanged (#3648) - Extend
CreateDeviceInterfacewith optional trailing topology PDA accounts (topology_count: u8); for Vpnv4 loopbacks under onchain allocation the processor allocates aFlexAlgoNodeSegmentper topology atomically with interface creation, so newly-provisioned devices no longer need a separateAssignTopologyNodeSegmentsstep. The CLI/SDK auto-discover existing topologies and pass them. Topology accounts are validated by program-owner and by first-byteAccountType::Topology(#3648) - Extend
UpdateDeviceInterfacewithupdate_topologies: bool+topology_count: u8to reconcile flex-algo node segments against a desired topology set on a Vpnv4 loopback (deallocate removed, allocate added, preserve unchanged); contributor owner or foundation allowlist can call it (#3648) DeleteDeviceInterfacenow deallocates flex-algo node segments alongside the loopback's base SR ID under onchain deallocation (#3648)- Canonicalize the forward-compatible Device interface names: rename the new struct from
NewInterfacetoInterface, the legacy enum fromInterfacetoInterfaceDeprecated, theDevice::new_interfacesfield toDevice::interfaces, and the legacyDevice::interfacesfield toDevice::deprecated_interfaces. On-disk format and behavior are unchanged — Borsh is positional, so the regenerated device fixture is byte-identical. Mechanical rename across the serviceability program, processors, CLI, sentinel, client, controlplane admin, and the Rust SDK. The activator/ crate was already deleted in an earlier change so the rename does not touch it (#3663) - Stop reading
Device::deprecated_interfacesoutside of backward-compat tests. Thedelete_deviceprocessor andDevice::validatenow check the canonicalDevice::interfacesvec; the legacy slot is treated as wire-format-only state populated by the deserializer and projected by the serializer. CLI, processor, and Rust SDK construction sites use..Default::default()to avoid initializing the legacy field. The unusedUpdateDeviceCommand::deprecated_interfacesSDK command field is removed.Device::deprecated_interfacesitself is retained for backward-compat fixture tests that verify legacy-slot decoding (#3663) - Remove the
CurrentInterfaceVersiontype alias and the unusedDevice::find_interface_legacyhelper. Tests that used to constructCurrentInterfaceVersion {...}(anInterfaceV2literal) and convert into either the canonicalInterfaceor the legacyInterfaceDeprecatedenum now buildInterface {...}directly. The legacy enum'sinto_current_version()method is replaced byto_v2()for the few backward-compat sites that still need anInterfaceV2projection (#3663) - Stop writing
InterfaceV3fromCreateDeviceInterfaceandUpdateDeviceInterface;CurrentInterfaceVersionis nowInterfaceV2.MigrateDeviceInterfacesandBackfillTopologycontinue to writeInterfaceV3since they are admin-controlled and need theflex_algo_node_segmentsfield - Add forward-compatible
NewInterfacestruct instate/interface.rswith asize: u16+version: u8on-disk prefix, V3-shaped body, andflex_algo_node_segments. Older readers can use the size prefix to skip past unknown future versions in constant time. Additive only — no callers, processors, or SDKs change in this PR (#3666) - Append
new_interfaces: Vec<NewInterface>toDeviceaftermax_multicast_publishers, behind a customBorshSerializethat projects the on-disk legacyinterfacesslot fromnew_interfaces(alwaysInterface::V2per #3653) and writesnew_interfacesat the end of the layout. Legacy accounts with no trailing bytes deserialize cleanly:Device::try_fromrebuildsnew_interfacesfrom the legacy enum vec via per-variantTryFrom. Older readers continue to parse the legacy slot at its existing offset; newer readers gain forward-compat via the trailing vec. Mutations now go throughDevice::replace_interface/push_interface/remove_interfaceso both vecs stay in sync;find_interfacereturns&NewInterfaceandfind_interface_legacyis a temporary helper for unrelated callers (#3665) - Migrate serviceability processors (
device/interface/{create,update,activate,delete,reject,remove,unlink},link/{accept,activate,closeaccount,create,delete,update},topology/backfill) to read and mutateDevice::new_interfacesdirectly.device.interfacesis no longer touched inprocessors/, andDevice::push_interfacenow takes aNewInterface.BackfillTopologyno longer mirrorsflex_algo_node_segmentsinto the legacy in-memoryinterfacesvec — segments live only innew_interfacesand are intentionally dropped on the V2-projected on-disk legacy slot (#3658) - Delete the
MigrateDeviceInterfacesprocessor and integration tests from the serviceability program.Device::TryFrom<&[u8]>(#3665) now auto-promotes legacyinterfacesintonew_interfaceswhen the trailing vec is missing, and the next account write persists the promoted vec — no standalone migration step is needed. Variant 111 is retained as aDeprecated111()tombstone (no-op dispatch, slot reserved so it isn't reused) for compatibility with older clients still emitting the old discriminator (#3662)
- Rename the
- SDK
- Go, Python, and TypeScript serviceability readers parse the trailing
new_interfacesvec onDevicewith size-prefixed (u16 size + u8 version + body) forward-compat framing. Empty trailing falls back to rebuildingnew_interfacesfrom the legacy enum vec, matching the Rust device reader. Length mismatch between the legacy and trailing vecs is surfaced as an error (Python/TS raise; Go setsDevice.DeserializeError). BumpsCURRENT_INTERFACE_VERSION/CurrentInterfaceVersionto4across SDKs to match Rust'sCURRENT_INTERFACE_SCHEMA_VERSION(#3660) - Regenerate
device.{bin,json}through Device's custom serializer with a populatednew_interfacesvec (one Vpnv4 loopback carrying aFlexAlgoNodeSegment, one physical user-tunnel-endpoint), and adddevice_legacy.{bin,json}(legacyinterfacesvec only, no trailing bytes — exercises the SDK legacy-fallback path) anddevice_future_version.{bin,json}(last trailing-vec element doctored toversion=5with 8 trailing junk bytes — exercises the SDK skip-to-end path). Adds fixture-driven Go SDK tests; extends the existing Python/TS fixture tests to cover all three Device fixtures (#3661) - Expose
SubscriptionStartSlotandLastUSDCPriceDollarson the shreds Go SDKClientSeatstruct, mapped to the existing prorated-billing fields in the onchain layout (#3684)
- Go, Python, and TypeScript serviceability readers parse the trailing
- SDK
- Apply the same rename in the Go, Python, and TypeScript serviceability readers: Go gets
Device.DeprecatedInterfaces↔Device.Interfaces(with the newInterfacestaking the trailing-vec slot previously held byNewInterfaces); Python getsDevice.deprecated_interfaces↔Device.interfaces; TypeScript getsDevice.deprecatedInterfaces↔Device.interfaces. TheInterface/DeviceInterfaceelement type in each SDK already represents the canonical (new) format and needs no rename. Length-mismatch error messages now reference the canonical field names (#3663) - Migrate read callers in the CLI, sentinel, client, controlplane admin, and Rust SDK topology helper to read interfaces from
Device::new_interfacesinstead of the legacyinterfacesenum vec, and adopt theDevice::find_interfacesignature that returns&NewInterface. The legacyinterfacesslot is still written on-disk via the per-write V2 projection from #3667; this PR only migrates reads. The temporaryDevice::find_interface_legacyhelper is retained for the smartcontract program processors, which migrate in a later issue. Activator is intentionally excluded — it is deprecated (#3659)
- Apply the same rename in the Go, Python, and TypeScript serviceability readers: Go gets
- Controller
- Stamp the default
UNICAST-DEFAULTtopology color on tunnels for users whose access pass has no tenant; previously the color was only resolved inside the tenant lookup branch, leaving tenantless users (the majority on mainnet/testnet) with no color community (#3648)
- Stamp the default
- CLI
doublezero device interface getdisplaysflex_algo_node_segmentsastopology_name:sr_idrows, falling back to a truncated pubkey when the topology cannot be looked up (#3648)
- Activator
- Delete the
activator/crate from the workspace; onchain allocation (RFC-11) supersedes it. The deployed activator was frozen in Phase 1 (#3608, #3628) and removed from e2e in Phase 2 (#3609, #3610, #3611, #3629). The*/activate,*/reject, and*/closeaccountonchain instructions and their SDK command modules remain in place for older CLIs until the min-version gate (#3612)
- Delete the
v0.21.0 - 2026-05-01
- Smartcontract
- Add
AccessPassType::EdgeSeat(Pubkey)variant to associate an access pass with a specific onchain Seat pubkey - Add
--accesspass-type edge-seat --seat <PUBKEY>toaccess-pass set - Add
--edge-seatand--seat-pubkeyfilters toaccess-pass list
- Add
- Client
- Add
--sock-fileglobal flag (aliases:--socket,--socket-path) to thedoublezeroCLI to override the default Unix socket path used to communicate withdoublezerod(/var/run/doublezerod/doublezerod.sock)
- Add
- Controller
- Fix unknown BGP peer cleanup in the Arista EOS template: hoist per-peer
no neighbor Xremoval into its ownrouter bgp 65342block so EOS's silent context-exit onno neighborfor a non-existent peer can't misroute subsequent peers' removal commands (#3627)
- Fix unknown BGP peer cleanup in the Arista EOS template: hoist per-peer
- Sentinel
- Pass dz_prefix resource accounts when creating the multicast user to force onchain allocation
- Smartcontract
- Allow ip_net to be passed when creating a device interface if the interface is CYOA/DIA/user_tunnel_endpoint
- Telemetry
- Add
agent_versionandagent_committoWriteDeviceLatencySamplesso the onchain header is refreshed on every write (~60s) instead of only at initialization; fixes stale version reporting after mid-epoch agent upgrades (#3598)
- Add
- CLI
doublezero -Vnow shows client version, program version, and minimum required version fetched from the serviceability program
v0.20.0 - 2026-04-29
- Telemetry
- Fix BGP status submitter to collect socket stats and tunnel interfaces from all tenant VRF namespaces (
ns-vrf<N>), not onlyns-vrf1; users whose tenant has a non-defaultVrfIdwere previously always reporting "tunnel not found" and had their onchain BGP status left stale - Fix BGP status submitter to collect from the root Linux network namespace when multicast users are present on a device; multicast GRE tunnels live in the global VRF (root namespace) rather than a per-tenant namespace, so their BGP sessions were never detected and onchain status remained permanently stale
- Fix BGP status submitter to collect socket stats and tunnel interfaces from all tenant VRF namespaces (
- CLI
- Add
--narrowflag todoublezero user listthat hideslocation,cyoa_type,accesspass, andtunnel_net, abbreviatesuser_type, and summarizesgroupsas one publisher entry plus one subscriber entry with independent+Noverflow counts; default output is unchanged - Add
doublezero-geolocation user update --user <code-or-pubkey> --token-account <pubkey>to update a geolocation user's payment token account; the underlyingUpdateGeolocationUserinstruction was already onchain but had no CLI entrypoint
- Add
- Smartcontract
- Allow
count > maxinDevice::validatefor all four per-device caps (max_users,max_unicast_users,max_multicast_subscribers,max_multicast_publishers) so operators can lower a cap below the live count; admission-time gates in user create still reject new connections when at capacity, letting the live count drain through natural churn
- Allow
v0.19.0 - 2026-04-24
- Controller
- Auto-loads
/etc/doublezero-controller/features.yamlat startup if present (silently skips if absent); whenflex_algo.enabled: true, populates topology data into the state cache, resolves tenant color communities fromTenant.include_topologies, and emits IS-IS flex-algo node segment and BGP color community stamping blocks into the Arista EOS template (disabled by default)
- Auto-loads
- SDK
- Go serviceability SDK adds
TopologyInfoaccount type withTopologyConstraint,IndexType/TopologyTypeaccount-type constants, andGetProgramDatadispatch case; extendsLinkwithLinkTopologiesandLinkFlags; extendsTenantwithIncludeTopologies
- Go serviceability SDK adds
- CLI
- Add
tunnel_endpointfield todoublezero user listoutput (table and JSON) showing the device-side GRE endpoint IP assigned to each user - Add
cyoa_ipsfield todoublezero device getanddoublezero device listoutput, showing the IP networks of interfaces withuser_tunnel_endpointenabled - Add
--tunnel_endpointflag touser updatecommand so operators can set the tunnel endpoint IP of an existing user - Extend
doublezero resource verifyto checkMulticastPublisherBlockagainst multicast publisher users'dz_ipallocations; legacydz_ips that fall outside the block's range are ignored so pre-existing users allocated before this extension existed do not produce false discrepancies - Fix
doublezero resource verifyto report missingTunnelIdsresource extensions for all devices, including those without any users; previously the discrepancy was suppressed when a device had no users, hiding unallocated extensions - Extend
doublezero resource verifyto detect orphanedResourceExtensionaccounts whose PDA does not correspond to any currently-expected resource type (global singleton or per-device extension for a live device/prefix);--fixcloses them via the existing y/N confirmation flow - Add
multicast subscribe,multicast unsubscribe,multicast publish, andmulticast unpublishCLI commands so users can modify their multicast role set on a connected session without runningdisconnect.unpublishwarns when the removal would drop the user's last publisher role (legacy-allocation environments may briefly reprovision in that case).
- Add
- Client
- Filter devices by type-specific capacity during auto-selection so clients are not provisioned onto devices that have reached their unicast, multicast publisher, or multicast subscriber limits
- Collector
- fallback to any probe if anchor probes aren't available
- Smartcontract
- Fix
BackfillTopologyaccount ordering: payer and system_program are now correctly placed after the variable-length device list, not before it - Fix
BackfillTopologySID collision: flex-algo node segment indices are now guaranteed not to duplicate any existing basenode_segment_idxvalue on the device - Fix multicast group allowlist add/remove for AccessPasses created with
allow_multiple_ip=true; the processors were rejecting requests with a real client IP because the stored IP is always0.0.0.0for these passes (#3551) - SDK now auto-detects the correct AccessPass PDA (static or dynamic) for allowlist operations based on whether an
allow_multiple_ippass exists - Add
doublezero link topology {create,delete,clear,backfill,list}subcommands for managing flex-algo topologies;topology clearauto-discovers tagged links when--linksis omitted - Add
TopologyInfoonchain account for IS-IS flex-algo link classification: auto-assigned TE admin-group bit (1–62), derived flex-algo number (128 + bit), and constraint type (include-any/include-all); capped at 62 topologies viaAdminGroupBitsresource extension - Add
link_topologies: Vec<Pubkey>(capped at 8) andlink_flags: u32(bit 0 = unicast-drained) to theLinkaccount - Add
include_topologiesto theTenantaccount for topology-filtered routing opt-in - Enforce UNICAST-DEFAULT topology existence as a precondition for link activation
- Extend
link getandlink listto display topology assignments and drain status; add--link-topology <name>filter tolink listand--link-topology(comma-separated topology names) /--unicast-drainedflags tolink update; usedefaultas the value to clear all topology assignments - Extend
tenant getandtenant listto display included topologies; add--include-topologies(comma-separated topology names) flag totenant update; usedefaultto clear
- Fix
- Sentinel
- Set a concrete
tunnel_endpointon multicast publisher create, preferring auser_tunnel_endpointinterface IP and falling back to the device'spublic_ip, excluding IPs already in use by another user at the sameclient_ip - Make the multicast publisher worker's
--client-filterflag repeatable so multiple validator client names can be matched in one run (OR semantics), matching the admin CLI behavior
- Set a concrete
- Tools
- Add
doublezero-admin migrate flex-algo [--dry-run]command to backfill link topology assignments and VPNv4 loopback flex-algo node segments across all existing devices and links
- Add
v0.18.0 - 2026-04-17
- Device Health Oracle
- Add
interface_countersactivation criterion to device-health-oracle to verify devices have recent interface counter data in ClickHouse before activation - Add
controller_successactivation criterion to device-health-oracle to verify devices have consistent controller call coverage over a configurable burn-in period by querying ClickHouse
- Add
- Telemetry
- Add
GET /device-link/agent-versionsendpoint to data-api andagent-versionssubcommand to data-cli, exposing per-device telemetry agent version and commit from onchainDeviceLatencySamplesHeader
- Add
- Smartcontract
- Allow
SubscribeMulticastGroupfor users inPendingstatus so thatCreateSubscribeUsercan be followed by additional subscribe calls before the activator runs (#3521) - Add optional
ownerfield toUpdateMulticastGroupinstruction, allowing foundation members to reassign ownership of a multicast group (#3527) - Rename
SubscribeMulticastGroupinstruction variant toUpdateMulticastGroupRolesand rename associated processor functions, args struct, and SDK command to use "roles" terminology, clarifying they manage publisher/subscriber roles rather than just subscriptions
- Allow
- Geolocation
- Add optional result destination to
GeolocationUserso LocationOffsets can be sent to an alternate endpoint instead of the target IP; supports both IP and domain destinations (e.g.,185.199.108.1:9000orresults.example.com:9000); includesSetResultDestinationonchain instruction, CLIuser set-result-destinationcommand, and Go SDK deserialization (backwards-compatible with existing accounts)
- Add optional result destination to
- CLI
- Add
--ownerflag tomulticast group update, accepting a pubkey orme(#3527) - Polish terminal output of
connectanddisconnect: fix emoji semantics, normalize message phrasing across IBRL and multicast code paths, resolve tenant to human-readable code on connect (errors if tenant not found), and fix progress bar not clearing before output indisconnect(#3529)
- Add
- Client
- Reduce default probing interval to 5m from 30s since DZDs don't generally move.
- Dependencies
- Bump vulnerable packages across Rust, Go, and Python to address Dependabot security alerts (14 packages fixed)
- DevContainer
- Add optional
DZ_WORKTREES_DIRmount exposing a host worktrees directory at/workspaces/worktreesinside the container; useful whendocker exec-ing into the persistent dev container to work on git worktrees outside the repo. Defaults to/tmp/worktrees(empty, harmless) when unset
- Add optional
v0.17.0 - 2026-04-10
- Activator
- Fix duplicate tunnel underlay pairs after restart by registering
device.public_ipas in-use for legacy users with unsettunnel_endpointduring allocation reload
- Fix duplicate tunnel underlay pairs after restart by registering
- Client
- Rank devices and tunnel endpoints by minimum observed latency (
min_latency_ns) instead of average when selecting a connection target, preferring paths with the best achievable round-trip time
- Rank devices and tunnel endpoints by minimum observed latency (
- Tools
- Add
IsRetryableFuncfield toRetryOptionsfor configurable retry criteria in the Solana JSON-RPC client; add"rate limited"string match and RPC code-32429to the default implementation
- Add
- Telemetry
- Add shared
telemetry/migrationspackage with goose-based ClickHouse schema migrations for all telemetry services; addCLICKHOUSE_RUN_MIGRATIONSenv var to flow-enricher and gnmi-writer for on-startup schema migration (#3460) - Add optional TLS support to state-ingest server via
--tls-cert-fileand--tls-key-fileflags; when set, the server listens on both HTTP (:8080) and HTTPS (:8443) simultaneously - Remove
--additional-child-probesCLI flag from telemetry-agent; child geoprobe discovery now relies entirely on the onchain Geolocation program - Add BGP status submitter: on each tick, reads BGP socket state from the device namespace, maps each activated user to their tunnel peer IP, and submits
SetUserBGPStatusonchain; supports a configurable down grace period and periodic keepalive refresh; enabled via--bgp-status-enablewith--bgp-status-interval,--bgp-status-refresh-interval, and--bgp-status-down-grace-periodflags - Bound
CachingFetcherRPC calls with an explicit 30s timeout;context.WithoutCanceldrops the parent deadline as well as cancellation, so without this a hung Solana RPC would block all singleflight waiters indefinitely
- Add shared
- Monitor
- Add ClickHouse as a telemetry backend for the global monitor alongside existing InfluxDB
- E2E tests
- Add
TestE2E_GeoprobeIcmpTargetsverifying end-to-end ICMP outbound offset delivery via onchainoutbound-icmptargets - Refactor geoprobe E2E tests to use testcontainers entrypoints and onchain target discovery
- Add
TestE2E_UserBGPStatusverifying that the telemetry BGP status submitter correctly reports onchain status transitions as clients connect and establish BGP sessions
- Add
- Monitor
- Add ClickHouse as a telemetry backend for the global monitor alongside existing InfluxDB
- SDK
- Deserialize
agent_versionandagent_commitfrom device latency samples in Go, TypeScript, and Python SDKs - Add
BGPStatustype (Unknown/Up/Down) andSetUserBGPStatusexecutor instruction to the Go serviceability SDK
- Deserialize
- Sentinel
- Improve
find-validator-multicast-publishersandcreate-validator-multicast-publisherswith multi-value--clientfilter,--ipfilter, nearest-device selection, dynamic capacity re-evaluation, and a fix for multicast publisher owner being set to the sentinel payer instead of the validator's owner
- Improve
- Smartcontract
- Add
agent_version([u8; 16]) andagent_commit([u8; 8]) fields toDeviceLatencySamplesHeader, carved from the existing reserved region; accept both fields in theInitializeDeviceLatencySamplesinstruction via incremental deserialization (fully backward compatible) - Implement
SetUserBGPStatusprocessor: validates metrics publisher authorization, updatesbgp_status,last_bgp_reported_at, andlast_bgp_up_atfields on the user account - Add human-readable error messages for serviceability program errors in the Go SDK, including program log extraction for enhanced debugging
user getno longer fails when no Access Pass exists; it prints a warning to stderr and continues, showing an empty access pass field- Replace manual account validation assertions with
validate_program_account!macro across serviceability processor files, adding consistentdata_is_emptychecks and fixing a missingis_writablevalidation inResumeLink(#3436) - Extend
validate_program_account!migration to remaining user and multicastgroup allowlist processors (set_bgp_status,delete,closeaccount, publisher/subscriberadd/remove) - Add
OutboundIcmptarget type (= 2) to the geolocation onchain program, enabling ICMP-based probing as an alternative to TWAMP for outbound geolocation targets - Allow pending users with subs to be deleted
- Add
- Onchain programs
- Add
tunnel_endpointfield to theUpdateUserinstruction (UserUpdateArgs), allowing the activator to overwrite a user's tunnel endpoint onchain; field is optional and backward compatible via incremental deserialization
- Add
- Telemetry
- Device telemetry agent now posts
agent_versionandagent_commitin theDeviceLatencySamplesHeaderwhen initializing new sample accounts, enabling version attribution of onchain telemetry data - Add optional TLS support to state-ingest server via
--tls-cert-fileand--tls-key-fileflags; when set, the server listens on both HTTP (:8080) and HTTPS (:8443) simultaneously - Remove
--additional-child-probesCLI flag from telemetry-agent; child geoprobe discovery now relies entirely on the onchain Geolocation program - Add BGP status submitter: on each tick, reads BGP socket state from the device namespace, maps each activated user to their tunnel peer IP, and submits
SetUserBGPStatusonchain; supports a configurable down grace period and periodic keepalive refresh; enabled via--bgp-status-enablewith--bgp-status-interval,--bgp-status-refresh-interval, and--bgp-status-down-grace-periodflags
- Device telemetry agent now posts
- Tools
- Add
IsRetryableFuncfield toRetryOptionsfor configurable retry criteria in the Solana JSON-RPC client; add"rate limited"string match and RPC code-32429to the default implementation
- Add
- Geolocation
- Standardize CLI flag naming: probe mutation commands use
--probe(was--code) accepting pubkey or code; rename--signing-keypair→--signing-pubkeyand--target-pk→--target-signing-pubkey; add--json-compacttogetcommands - geoprobe-target can now store LocationOffset messages in ClickHouse
- Add ICMP pinger to geoprobe-agent for measuring outbound ICMP targets with interleaved batch send/receive, integrated into the existing measurement cycle alongside TWAMP
- Remove
--additional-parent,--additional-targets,--additional-icmp-targets, and--allowed-pubkeysCLI flags from geoprobe-agent; all configuration now comes from onchain state via parent and target discovery - Add
MinCachefor tracking minimum-RTT measurements with best/backup promotion over a rolling TTL window, used by both geoprobe-target and geoprobe-target-sender to suppress redundant output and surface only new-best events
- Standardize CLI flag naming: probe mutation commands use
v0.16.0 - 2026-04-03
- Smartcontract
- Require that the access pass provided to
SubscribeMulticastGroupbelongs to the payer; foundation allowlist members may use any access pass. - Add Index account for onchain key uniqueness enforcement and O(1) key-to-pubkey lookup, with standalone CreateIndex/DeleteIndex instructions for migration backfill
- Set minimum client version to 0.10.0
- Enforce 9000-byte MTU on links and non-CYOA/non-DIA device interfaces; CYOA/DIA interfaces must be 1500. Onchain validation now returns
InvalidMtu(error 46) for non-conforming values. - Add
OutboundIcmptarget type (= 2) to the geolocation onchain program, enabling ICMP-based probing as an alternative to TWAMP for outbound geolocation targets
- Require that the access pass provided to
- CLI
- Allow incremental multicast group addition without disconnecting
- Reset SIGPIPE to SIG_DFL at the start of main() in all 3 CLI binaries (doublezero, doublezero-geolocation, doublezero-admin) so the process exits silently like standard CLI tools
- Support
--type outbound-icmpin geolocationuser add-target,remove-target, andgetcommands - Add sentinel admin commands to find and create multicast publishers for IBRL validators
- handle non-user owned disconnects gracefully
- Add user's multicast pub/sub groups if applicable to
status
- Sentinel
- Add multicast publisher worker with Solana RPC-based validator discovery
- Add e2e tests for multicast publisher worker with validator-metadata-service mock
- SDK
- Add Go SDK for shred subscription program with read-only account deserialization (epoch state, seat assignments, pricing, settlement, validator client rewards), PDA derivation helpers, RPC fetchers, compatibility tests, and a fetch example CLI
- Add
GeoLocationTargetTypeOutboundIcmpto Go geolocation SDK with deserialization and round-trip test support
- Device Health Oracle
- Update link.health and device.health to
ready-for-serviceandready-for-userswhen they are not already in that state
- Update link.health and device.health to
- Tools
- Add
twamp-debugdiagnostic tool for testing kernel timestamping support on switches; sends real TWAMP probes to verify which SO_TIMESTAMPING modes (RX/TX software/hardware/sched) actually deliver timestamps, and reports RTT statistics comparing userspace vs kernel timestamp sources
- Add
- E2E Tests
- Switch backward compatibility test to install versioned CLI binaries from GitHub releases instead of Cloudsmith apt repos; version enumeration now uses the GitHub API directly from Go rather than querying apt-cache inside the container
- Client
- Add
doublezero_connection_infoPrometheus metric exposing connection metadata (user_type, network, current_device, metro, tunnel_name, tunnel_src, tunnel_dst) (#3201) - Add
doublezero_connection_rtt_nanosecondsanddoublezero_connection_loss_percentagePrometheus metrics reporting RTT and packet loss to the current connected device
- Add
v0.15.0 - 2026-03-27
- Client
- Funder
- Top up contributor owner keys alongside device metrics publishers, multicast group owners, and the internet latency collector
- Smartcontract
- Fix multicast publisher/subscriber device counter divergence:
multicast_publishers_countnever decremented andmulticast_subscribers_countover-decremented on user disconnect because the decrement logic checked!publishers.is_empty(), which is always false at delete time. Add a durabletunnel_flagsfield to theUserstruct with aCreatedAsPublisherbit, set at activation, and use it in the delete and closeaccount instructions. - Allow foundation allowlist members and the sentinel to create multicast users with a custom
ownervia a newownerfield onCreateSubscribeUser, enabling user creation on behalf of another identity's access pass
- Fix multicast publisher/subscriber device counter divergence:
- CLI
- Add
--ownerflag todoublezero user create-subscribefor specifying a custom user owner (foundation/sentinel only)
- Add
v0.14.0 - 2026-03-24
- Controller
- Log an error when duplicate tunnel-id assignments are detected on the same device during state cache update, instead of silently overwriting
- Onchain Programs
- Serviceability: update device interface IPs when
tunnel_netis changed viaUpdateLink, matching the existingActivateLinkbehavior (#3365) - Serviceability:
AcceptLinksupports combined accept+activate viause_onchain_allocationflag, gated onOnChainAllocationfeature flag (#3369) - Serviceability: add
feed_authoritytoRemoveMulticastGroupSubAllowlistauth check, matchingAddMulticastGroupSubAllowlist
- Serviceability: update device interface IPs when
- Client
- Get client IP from the daemon in the disconnect command, matching the connect command's behavior, to avoid IP mismatches behind NAT
- Onchain Programs
- Add
target_update_countfield to GeoProbe account, incremented onAddTargetandRemoveTarget; usesBorshDeserializeIncrementalso existing accounts default to 0 (non-breaking)
- Add
- SDK
- Add
TargetUpdateCountfield to Go GeoProbe struct with backward-compatible deserialization
- Add
- Telemetry
- Skip expensive
GetGeolocationUsersRPC scan in geoprobe-agent when the probe'starget_update_countis unchanged, with a forced full refresh every ~5 minutes as safety net - Add Prometheus metrics to geoprobe-agent: build info, error counters by type, discovery/measurement cycle durations, offset send/receive/reject counters, and discovered target/parent gauges; exposed via optional
--metrics-enableflag
- Skip expensive
v0.13.0 - 2026-03-20
- Activator
- Reserve device allocations (loopback IPs and segment routing IDs) for devices in Drained, DeviceProvisioning, and LinkProvisioning states at startup, preventing collisions with new device allocations
- Reserve user allocations (tunnel_net, tunnel_id, dz_ip, publisher IPs, tunnel endpoints) for users in Updating and OutOfCredits states at startup, preventing collisions with new user allocations
- Fix duplicate tunnel_net/tunnel_id allocation by reserving addresses for links in HardDrained, SoftDrained, and Provisioning states during startup initialization
- Onchain Programs
- Allow foundation to remove targets from GeolocationUser accounts via the
RemoveTargetinstruction, unblocking foundation-initiated user deletion when targets still exist - Serviceability: fix
SubscribeMulticastGroupderiving the AccessPass PDA frompayer_account.keyinstead ofuser.owner, which causeduser deleteto fail with "Invalid AccessPass PDA" when a foundation allowlist key signed and the user had active multicast subscriptions - feat(smartcontract): add tunnel_net/tunnel_id reallocation to UpdateLink (#3326)
- Allow foundation to remove targets from GeolocationUser accounts via the
- Client
- Fix
v2/statusreturning emptycurrent_deviceandmetrofor multicast subscribers by adding aclientIP + UserTypefallback in status enrichment when DzIp and tunnel_dst matching both fail - Set tunnel interface administratively down before deleting during teardown, so external applications with sockets bound to the tunnel's overlay IP receive errors before the interface is removed
- Fix
- CLI
- Include feed authority in
global-config authority getoutput - Add
geolocation usersubcommands to manage GeolocationUser accounts and targets:create,delete,get,list,add-target,remove-target, andupdate-payment-status
- Include feed authority in
- Monitor
- Fix slack user reporting
- SDK
- Add GeolocationUser types, Borsh deserialization, PDA derivation, and read-only client methods (
GetGeolocationUserByCode,GetGeolocationUsers) to the Go geolocation SDK
- Add GeolocationUser types, Borsh deserialization, PDA derivation, and read-only client methods (
- Telemetry
- Add onchain target discovery to the geoProbe agent: polls GeolocationUser accounts at 60s intervals, filters for activated+paid users, dynamically updates outbound probe targets and inbound signed TWAMP authorized keys
- Fix geoProbe parent discovery incorrectly adding parent authority keys to the signed TWAMP reflector allowlist; parent DZDs use the unsigned reflector
- Controller
- Retry transient Solana RPC failures when fetching onchain serviceability accounts so controller polls are more resilient to short-lived provider resets
v0.12.0 - 2026-03-16
- Onchain Programs
- Add
GeolocationUseraccount state to the geolocation program with owner, billing config (flat-per-epoch), payment/user status, and a target list supporting outbound (IP + port) and inbound (pubkey) geolocation targets - Add GeolocationUser CRUD instruction processors (create, update, delete) with owner authorization and target-empty guard on delete
- Add GeolocationUser target management (AddTarget, RemoveTarget) with duplicate/not-found guards, public IP validation, MAX_TARGETS limit, and GeoProbe reference count tracking
- Add foundation-only UpdatePaymentStatus instruction to set payment status and last deduction epoch on GeolocationUser billing config
- Add
- Client
- Demote passive-mode liveness session-down log messages from Info to Debug to reduce log noise when no dataplane action is taken
- Telemetry
- Add
Version(uint8) andTargetIP([4]byte) fields to LocationOffset wire format (v1, 174 bytes), with version validation on unmarshal to enable safe future format evolution
- Add
- Tools
- Update TWAMP signed packet parser byte offsets and
OffsetInfostruct for LocationOffset v1 layout
- Update TWAMP signed packet parser byte offsets and
- Onchain Programs
- Serviceability: allow reservation authority to create, update, and close access passes, with ownership restriction preventing modification of passes created by other authorities
- Serviceability: fix multicast user creation failing when access-pass has a
tenant_allowlist; tenant enforcement only applies to unicast connections since multicast users are not tenant-scoped - Serviceability: rename
reservation_authority_pktofeed_authority_pkin GlobalState and renameRESERVATIONpermission flag toFEED_AUTHORITY - Serviceability: remove
ReserveConnection,CloseReservation,CreateReservedSubscribeUser,DeleteReservedSubscribeUserinstructions andReservationaccount type
- E2E Tests
- Fix
TestE2E_UserLimitsnot asserting command failure: the; echo EXIT_CODE=$?pattern caused the shell to always exit 0 regardless of thedoublezeroexit code, makingerralways nil; replace withrequire.Errorassertions so the test fails if a limit-exceeded connect unexpectedly succeeds - Fix intermittent flake in
TestE2E_MultiClientIBRL_RouteLiveness: add explicit route convergence checks after each unblock before starting the next block/unblock cycle, ensuring the liveness subsystem and BGP have fully settled - Add geoprobe E2E test (
TestE2E_GeoprobeDiscovery) that exercises the full geolocation flow: deploy geolocation program, create probe onchain, start geoprobe-agent container, and verify the telemetry-agent discovers and measures the probe via TWAMP - Add geoprobe Docker image, geolocation program build/deploy support, and manager geolocation CLI configuration to the E2E devnet infrastructure
- Extend geoprobe E2E test with outbound offset forwarding (agent → target with signature chain verification) and inbound signed TWAMP probing (target-sender → agent with DZD offset embedding); build geoprobe-target and geoprobe-target-sender binaries into the geoprobe Docker image
- Fix
- Telemetry
- Add onchain parent DZD discovery to geoprobe-agent: periodically queries the Geolocation program for this probe's parent devices and resolves their metrics publisher keys from Serviceability, replacing the need for static
--parent-dzdCLI flags. Static parents from CLI are merged with onchain parents, with onchain taking precedence for duplicate keys. - Optimize inbound probe-measured RTT accuracy: pre-sign both TWAMP probes before network I/O so probe 1 fires immediately after reply 0 with no signing delay, measure Tx-to-Rx interval (reply 0 Tx → probe 1 Rx) instead of Rx-to-Rx to exclude processing overhead on both sides, use kernel
SO_TIMESTAMPNSreceive timestamps on the reflector, and add a 15ms busy-poll window on the sender to avoid scheduler wakeup latency - Optimize outbound probe RTT accuracy: send a staggered warmup probe on a separate socket 2ms before the measurement probe to wake the reflector's thread, then take the min RTT of both
- Add onchain parent DZD discovery to geoprobe-agent: periodically queries the Geolocation program for this probe's parent devices and resolves their metrics publisher keys from Serviceability, replacing the need for static
- Onchain Programs
- Serviceability: add
Permissionaccount withCreatePermission,UpdatePermission,DeletePermission,SuspendPermission, andResumePermissioninstructions for managing per-keypair permission bitmasks onchain - Serviceability: add
TOPOLOGY_ADMIN,RESOURCE_ADMIN, andINDEX_ADMINpermission flags for delegating management of segment-routing topologies, ResourceExtension accounts, and internal Index accounts (legacy authorization maps each to the foundation allowlist) - Serviceability: enforce
TOPOLOGY_ADMIN/RESOURCE_ADMIN/INDEX_ADMINviaauthorize()in the topology (create/delete/clear/assign-node-segments), resource (create/allocate/deallocate/close), and index (create/delete) instructions, which were previously gated by the foundation allowlist only - Serviceability: fix
ClearTopologyaccount layout — the processor now parsespayer/system_program/permissionfrom the tail of the account list (matching what the SDK client appends after the variable-length link list) instead of reading them at fixed front positions, sodoublezero topology clearno longer reverts when links are passed - Serviceability:
authorize()now falls back to the legacy allowlists when a payer's auto-injected Permission account exists but does not grant the requested flag, as long asRequirePermissionAccountsis off — so foundation (and other legacy-authorized) keys are not locked out of an instruction merely because they also hold an unrelated, under-privileged Permission account
- Serviceability: add
- SDK
- Add
execute_authorized_transaction(and its_quietvariant) alongsideexecute_transaction. The authorized variants append the payer's Permission PDA (read-only) as the trailing account when it exists on-chain, soauthorize()can find it. All variants share the same builder, so the protocol-max compute-budget/heap-frame requests, preflight, and error-reporting behavior are identical toexecute_transaction; the only difference is the optional trailing Permission account. The Permission PDA lookup is retried on transient RPC errors and memoized per client. - Add
TOPOLOGY_ADMIN/RESOURCE_ADMIN/INDEX_ADMINpermission-flag constants to the Go, TypeScript, and Python serviceability SDKs
- Add
- CLI
- Add
permission get,permission list, andpermission setcommands with table and JSON output;permission setsupports incremental--add/--removeflags and creates or updates the account as needed - Add
topology-admin,resource-admin, andindex-adminto the named permissions accepted bypermission set --add/--remove
- Add
v0.11.0 - 2026-03-12
- Onchain Programs
- Serviceability: split per-device multicast user tracking into separate subscriber and publisher counters (
multicast_subscribers_count/max_multicast_subscribersandmulticast_publishers_count/max_multicast_publishers); publisher and subscriber limits are now enforced independently
- Serviceability: split per-device multicast user tracking into separate subscriber and publisher counters (
- Controller
- Downgrade transient ClickHouse write errors from ERROR to WARN, escalating to ERROR only after 3 consecutive flush failures to reduce alert noise (#3220)
- Telemetry
- Detect and replace unresponsive RIPE Atlas source probes that stop returning ping results, with a 24-hour TTL on the unresponsive probe blacklist so probes are retried after expiry
- Compare source probe IDs (not just location codes) during measurement reconciliation so that probe replacements trigger measurement recreation
- Fix race condition in internet-latency-collector where export and management goroutines independently loaded/saved the same state file, causing newly created RIPE Atlas measurement metadata to be overwritten and measurements to be stuck in a create-destroy-create loop (#3195)
- Add wheresitup job backlog observability:
pending_jobsPrometheus gauge,in_progress_count/pending_jobsin export summary logs, and API response duration histogram (#3203) - Change geoprobe-agent and geoprobe-target default TWAMP reflector port from 862 to 8925 to avoid DZD ACL blocks, use per-probe TWAMP port instead of hardcoded constant, and update
--additional-child-probes/--additional-targetsformat tohostorhost:offset_port:twamp_port(two-fieldhost:portrejected as ambiguous)
- Activator
- Fix tunnel ID leak in user activation: eagerly-allocated tunnel_id, tunnel_net, and dz_ip were not rolled back when the activation transaction failed, causing ghost IDs to accumulate and eventually exhaust the controller's tunnel slot range
- Suppress noisy program log output from race conditions caused by dual event processing (websocket + snapshot poll). The SDK's new
execute_transaction_quietreturns aSimulationErrorwith program logs; the activator verifies suspected races by re-fetching user state before deciding whether to print logs (#3197)
- CLI
- Add
doublezero-geolocationCLI for managing geolocation program entities: GeoProbe CRUD (create, get, list, update, delete), parent device management (add/remove), program config initialization, and geolocation-specific config get/set - Add
--multicast-publishers-countand--multicast-subscribers-countflags todevice updatefor foundation-gated count correction; rename--max-multicast-usersto--max-multicast-subscribersand add--max-multicast-publishers - Add
doublezero-admin device migrate-multicast-counts [--dry-run]to correct stalemulticast_subscribers_count/multicast_publishers_counton existing deployments where all multicast users were previously counted as subscribers; supports dry-run preview and continues past per-device failures - Add
doublezero-admin device migrate-unicast-counts [--dry-run]to correct staleunicast_users_counton all devices; same behaviour as the multicast counts command
- Add
- SDK
- Add read-only Go SDK for
doublezero-geolocationprogram with state deserialization, PDA derivation, and RPC client for querying geoprobe configuration - Add
GetGeoProbeKeysto geolocation SDK for lightweight account key fetching using DataSlice to minimize RPC bandwidth - Add Rust SDK instruction builders for GeolocationUser management: CRUD (create, update, delete), target management (add, remove), payment status updates, and read queries (get by code/pubkey, list all)
- Add read-only Go SDK for
- Telemetry
- Add onchain GeoProbe discovery to the telemetry agent: periodically queries the Geolocation program for child probes parented to the local device, replacing the need for
--additional-child-probesCLI flag - Embed LocationOffsets from parent DZDs in signed TWAMP replies so inbound probes carry geolocation context, and make signed TWAMP replies more like LocationOffsets to couple with a new double-probe system for inbound probing.
- Add onchain GeoProbe discovery to the telemetry agent: periodically queries the Geolocation program for child probes parented to the local device, replacing the need for
- Client
- Fix mainnet-beta Cloudsmith package containing the testnet binary by giving the mainnet-beta Rust build a separate
CARGO_TARGET_DIRto prevent GoReleaser artifact collision - Increase default onchain fetch timeout from 20s to 60s to improve resilience on high-latency RPC paths; add
-reconciler-fetch-timeoutflag to allow operators to override - Add prometheus metrics for onchain RPC fetches: fetch duration histogram, result counter (success/error with stale cache/error with no cache), and stale cache age gauge
- Increase default route liveness probe interval (TxMin/RxMin) from 300ms to 1s and raise MaxTxCeil from 1s to 3s to preserve backoff headroom
- Throttle O(n) per-service scheduler queue length metric from every event to once per 10s to fix excessive CPU usage on nodes with many liveness sessions
- Fix mainnet-beta Cloudsmith package containing the testnet binary by giving the mainnet-beta Rust build a separate
- Smartcontract
- Serviceability: add foundation-only
unicast_users_count,multicast_subscribers_count, andmulticast_publishers_countfields toUpdateDeviceinstruction for direct count correction, with corresponding--unicast-users-count,--multicast-subscribers-count, and--multicast-publishers-countCLI flags - Serviceability: fix
validate_account_codeforcing lowercase on all entity types — restrict lowercase normalization to device and link codes only, preserving original case for locations, exchanges, contributors, and other entities - feat(smartcontract): atomic onchain allocation for CreateDevice (#3216)
- Serviceability: RequestBanUser instruction supports atomic deallocate when OnchainAllocation feature is enabled
- Serviceability: DeviceInterfaceUpdate instruction supports onchain allocation of node_segment_idx
- Serviceability: fix
validate_account_codeforcing lowercase on all entity types — restrict lowercase normalization to device and link codes only, preserving original case for locations, exchanges, contributors, and other entities - feat(smartcontract): atomic onchain allocation for CreateDevice (#3216)
- Serviceability: CreateDeviceInterface instruction supports atomic create+allocate+activate when OnchainAllocation feature is enabled
- Serviceability: DeleteDeviceInterface instruction supports atomic deallocate+remove when OnchainAllocation feature is enabled
- Serviceability: redesign reservations from per-IP accounts to per-(device, owner) reservation blocks with a configurable seat count, and integrate with user creation so creating a user can consume a reserved seat (#3224)
- Serviceability: add foundation-only
- CLI
- Add
access-pass user-balancescommand to show per-payer SOL balance, required amount (rent + gas reserve), and missing amount, with filters (--user-payer,--min-balance,--max-balance,--min-missing,--max-missing), sorting, and--top N - Add
access-pass fundcommand to top up underfunded user payers, with--dry-run,--force(skip confirmation),--min-balance, and a pre-transfer sender balance check; required balance floor includes a gas-fee reserve (50 × 5,000 lamports) and the wallet rent-exempt minimum to prevent on-chain transfer failures - Fix
access-pass fundandaccess-pass user-balancesslot counting to track remaining slots per(payer, client_ip)pair, preventing a connected user on IP_B from consuming open slots for IP_A; also fix required balance formula to always addwallet_rent_minon top ofneeds_rentrather than taking the max - Add
--user-payerfilter touser listcommand - Serviceability: onchain activation - atomic close for DeleteDevice (#3188)
- Fix
access-pass user-balancesandaccess-pass fundunderestimating the required wallet balance: the wallet's own rent-exempt minimum was used as a floor rather than being added, causingmissing: 0to be reported even when provisioning would fail with insufficient funds (#3213)
- Add
- Onchain Programs
- Serviceability: DeleteUser instruction supports atomic deallocate+closeaccount when OnchainAllocation feature is enabled
- Serviceability: CreateLink instruction supports atomic create+allocate+activate when OnchainAllocation feature is enabled
- Serviceability: DeleteLink instruction supports atomic deallocate+closeaccount when OnchainAllocation feature is enabled
- Serviceability: CreateMulticastGroup instruction supports atomic create+allocate+activate when OnchainAllocation feature is enabled
- Serviceability: DeleteMulticastGroup instruction supports atomic deallocate+closeaccount when OnchainAllocation feature is enabled
- Serviceability: CreateSubscribeUser instruction supports atomic create+allocate+activate when OnchainAllocation feature is enabled
- Serviceability: MulitcastGroupUpdate instruction supports atomic ip allocation when OnchainAllocation feature is enabled
- Serviceability: UpdateUser instruction supports atomic allocate+deallocate when OnchainAllocation feature is enabled
v0.10.0 - 2026-03-04
- CLI
doublezero resource verifycommand will now suggest creating resources or create them with --fix- Print an explicit message when the tenant is resolved implicitly from the configuration file (
Using tenant '...' from configuration file.) or from the Access Pass allowlist (Using tenant '...' from Access Pass.) - All
getcommands now display output as a formatted table and support a--jsonflag for machine-readable output - Remove
solana-multicast-publisherandsolana-multicast-subscriberfromaccess-pass settype options (multicast roles use the tenant field on aprepaidpass instead); make--solana-validatoroptional forsolana-rpctype getcommands now expose all onchain account fields:user getaddstunnel_id,tunnel_endpoint, andvalidator_pubkey;device getaddsreserved_seats;link getaddstunnel_id;multicastgroup getaddstenant,publisher_count, andsubscriber_count;tenant getaddspayment_status,billing,administrators, andtoken_account;exchange getaddsdevice1_pkanddevice2_pk
- SDK
- Fix multicast group deserialization in
smartcontract/sdk/goto correctly read publisher and subscriber counts and align status enum with onchain definition
- Fix multicast group deserialization in
- Smartcontract
- Serviceability: add
Reservationaccount andReserveConnection/CloseReservationinstructions for pre-reserving connection seats on devices, withreserved_seatsfactored into capacity checks on both reservation and user creation - Allow sentinel authority to add/remove multicast publisher and subscriber allowlist entries
- Serviceability: add
- SDK
- Add Rust SDK for geolocation program with
GeoClient, GeoProbe CRUD operations (create, get, list, update, delete, add/remove parent device), and ProgramConfig management (init, get, update) - Add
geo_program_idtoNetworkConfigfor geolocation program discovery
- Add Rust SDK for geolocation program with
- Telemetry
- Add
geoprobe-target-senderCLI tool for sending signed TWAMP probes to a GeoProbe and verifying signed replies (RFC16 inbound probing flow) - Add Signed TWAMP reflector to geoprobe-agent with configurable listen port and allowed-pubkeys allowlist
- Fix global monitor crash when IBRL and multicast users share the same client IP but are on different devices, by preferring non-multicast users in client IP lookups to match status device selection
- Add
- Client
- Add onchain reconciler to daemon — automatically provisions/removes tunnels by polling onchain User state, replacing CLI-driven provisioning and the
doublezerod.jsonstate file (RFC-17) - Add
doublezero enable/doublezero disableCLI commands to toggle the reconciler at runtime
- Add onchain reconciler to daemon — automatically provisions/removes tunnels by polling onchain User state, replacing CLI-driven provisioning and the
- E2E tests
- Publish
TestQA_AllDevices_UnicastConnectivityresults to ClickHouse (qa_alldevices_resultsandqa_alldevices_metadatatables) in addition to InfluxDB; configured viaCLICKHOUSE_ADDRenv var, skipped gracefully when not set
- Publish
- Onchain Programs
- Serviceability: CreateUser instruction supports atomic create+allocate+activate when OnchainAllocation feature is enabled
v0.9.0 - 2026-02-27
- CLI:
--bandwidthis now a required argument fordoublezero device interface createanddoublezero device interface update; callers that previously omitted it (relying on the default of0) must now explicitly pass a value with a unit (e.g.--bandwidth 10Gbps)
- Tools
- Add signed TWAMP sender and reflector with Ed25519 authentication and per-pubkey rate-limited signature verification
- Activator
- Adds a stateless mode for when onchain allocation is enabled. This prevents activator/onchain from becoming out-of-sync.
--onchain-allocationcli flag replaced with feature flag from GlobalState onchain
- SDK
- Add retry with exponential backoff (3 retries, 500ms–5s) to all read-only RPC calls in
DZClient, improving resilience to transient RPC timeouts
- Add retry with exponential backoff (3 retries, 500ms–5s) to all read-only RPC calls in
- CLI
- Fix
doublezero statusshowing "Current Device" and "Metro" as N/A for multicast subscribers when the tunnel destination is auser_tunnel_endpointloopback interface IP rather than the device'spublic_ip - Remove redundant
connect ibrlunit tests that were duplicates of hybrid-device equivalents doublezero global-config feature-flagscommands added- Fix multicast subscriber tunnel source resolution for NAT environments — resolve local interface IP instead of using public IP
- Added multicast filters to access-pass list, enabling filtering by publisher/subscriber role and identifying access passes not authorized for a specific multicast group.
- Device interface
--bandwidthand--cirflags now accept Kbps, Mbps, or Gbps units;interface listdisplays those values as human-readable strings - Add duplicate IP check to prevent a user from assigning the same IP more than once
- Fix
- Client
- Fix BGP
OnClosedeleting routes from all peers instead of only the closing peer, preventing multicast teardown from nuking unicast routes - Skip route deletion on
OnCloseforNoInstallpeers (multicast) since they never install kernel routes - Reject BGP martian addresses (CGNAT, multicast, reserved, benchmarking, etc.) as client IP during
connect
- Fix BGP
- Controller
- detect duplicate (UnderlaySrcIP, UnderlayDstIP) pairs for tunnels and only render the first to the device config and write a log error for the second
- allow MTU to be configurable
- Onchain Programs
- Serviceability: skip field validation for users in
Deletingstatus to prevent accounts from getting stuck during cleanup when validation rules change - Serviceability: require foundation_allowlist privileges to update node_segment_idx on a device interface
- Serviceability: add feature flags support
- Serviceability: expand
is_globalto reject all BGP martian address ranges (CGNAT 100.64/10, IETF 192.0.0/24, benchmarking 198.18/15, multicast 224/4, reserved 240/4, 0/8) - Serviceability: allow update and deletion of interfaces even when sibling interfaces have invalid CYOA configuration
- Geolocation: add
doublezero-geolocationprogram scaffolding and GeoProbe account type and related instructions as per rfcs/rfc16-geolocation-verification.md
- Serviceability: skip field validation for users in
- SDK
- SetFeatureFlagCommand added to manage on-chain feature flags for conditional behavior rollouts
- Dependencies
- Upgrade Solana SDK workspace dependencies from 2.2.7 to 2.3.x (
solana-sdk,solana-client,solana-program-test, and others)
- Upgrade Solana SDK workspace dependencies from 2.2.7 to 2.3.x (
- Internet telemetry
- Reduce RIPE Atlas sampling interval from 6 minutes to 10 minutes to work around service limit of 100_000 samples per day
- E2E
- e2e: add multi-tenant access control negative tests (#3081)
v0.8.11 - 2026-02-27
- N/A
- Client
- Fix BGP
OnClosedeleting routes from all peers instead of only the closing peer, preventing multicast teardown from nuking unicast routes
- Fix BGP
v0.8.10 – 2026-02-19
- N/A
- Activator
- removes accesspass monitor task (that expires access passes)
- Monitor
- Add Prometheus metrics for multicast publisher block utilization (
doublezero_multicast_publisher_block_total_ips,doublezero_multicast_publisher_block_allocated_ips,doublezero_multicast_publisher_block_utilization_percent) — enables Grafana alerting on IP pool exhaustion thresholds - Include tenant name in the Slack notification table when new users are activated
- Add Prometheus metrics for multicast publisher block utilization (
- SDK (Go)
- Add
GetMulticastPublisherBlockResourceExtension()method to serviceability client for fetching the global multicast publisher IP allocation bitmap - Fix LinkDesiredStatus discriminants (hard-drained=6, soft-drained=7)
- Add
- Onchain Programs
- Upgrade programs from system_instruction to solana_system_interface
- Refactor user creation to validate all limits (max_users, max_multicast_users, max_unicast_users) before incrementing counters — improves efficiency by avoiding wasted work on validation failures and follows fail-fast best practice
- Serviceability:
UnlinkDeviceInterfacenow only allowsActivatedorPendinginterfaces; when an associated link account is provided for anActivatedinterface, the link must be inDeletingstatus - Links and devices can no longer be deleted from
Activatedstatus — must be drained first; deletion is rejected withInvalidStatus - Contributors, locations, multicast groups, and users can now be deleted from any operational status (not just
Activated); onlyDeleting/Updatingstates are blocked - SDK:
UnlinkDeviceInterfaceCommandautomatically discovers and passes associated link accounts - Serviceability: allow contributors to update prefixes when for IBRL when no users are allocated
- CLI
doublezero statusnow shows aTenantcolumn (betweenUser TypeandCurrent Device) with the tenant code associated with the user; empty when no tenant is assigned
- Client
- Fix tunnel overlay address scope to prevent kernel from selecting link-local /31 as source for routed traffic
- Serviceability: close orphaned blocks when dz_prefixes shrink
- Serviceability: require ip-net for CYOA interfaces
- E2E / QA Tests
- Fix QA unicast test flake caused by RPC 429 rate limiting during concurrent user deletion — treat transient RPC errors as non-fatal in the deletion polling loop
- Backward compatibility test: use
--statusinstead of--desired-statusfor drain commands; fix version ranges (link drain compatible since v0.7.2, device drain since v0.8.1) - Add daily devnet QA test for device provisioning lifecycle (RFC12) — deletes/recreates device and links, restarts daemons with new pubkey via Ansible
- Remove devnet/testnet environment filter from
TestQA_MulticastPublisherMultipleGroups— test now runs against all environments
v0.8.9 – 2026-02-16
- N/A
- Activator
- Fail to start if any global config network blocks (
device_tunnel_block,user_tunnel_block,multicastgroup_block,multicast_publisher_block) are unset (0.0.0.0/0) - Fix multicast publisher dz_ip leak in offchain deallocation — IPs from the global publisher pool were never freed on user deletion because the check required non-empty publishers, which the smartcontract already clears before allowing deletion
- Fail to start if any global config network blocks (
- Client
- Fix heartbeat sender not restarting after disconnect due to poisoned done channel
- Onchain Programs
- bugfix(serviceability): contributors can now update their interfaces, CYOA interfaces saved on create, physical interfaces remain after unlink (#2993)
- Device controller
- Reject users with BGP martian DZ IPs (RFC 1918, loopback, multicast, link-local, documentation nets, shared address space, reserved) to prevent invalid addresses from being advertised via BGP or permitted in device ACLs
- Claude
- chore: update CLAUDE.md (#2999)
- e2e
- e2e: Add network contributor e2e flow tests (#2997)
v0.8.8 – 2026-02-13
- None for this release
- Activator
- Assign multicast publisher IPs from global pool in serviceability GlobalConfig instead of per-device blocks
- Client
- Add multicast publisher heartbeat sender — sends periodic UDP packets to each multicast group to keep PIM (S,G) mroute state alive on devices
- Fix panic in heartbeat sender when concurrent teardown requests race on close
- E2E tests
- Add daily devnet QA test for device provisioning lifecycle (RFC12) — deletes/recreates device and links, restarts daemons with new pubkey via Ansible
- Serviceability: prevent creating or activating links on interfaces with CYOA or DIA assignments, and prevent setting CYOA/DIA on interfaces that are already linked
- CLI: add early validation in
link wan-createandlink dzx-createto reject interfaces with CYOA or DIA assignments
v0.8.7 – 2026-02-10
- None for this release
- SDK
- Added Tenant to all sdks
- E2E tests
- Added multi-tenancy deletion test coverage
- Telemetry
- Add
doublezero-geoprobe-agent, intermediary probe server for RFC16 - Adds support for per-tenant metro routing policy
- Add
--geoprobe-pubkeyflag todoublezero-geoprobe-agentfor device identity LocationOffsetstruct now includesSenderPubkeyto distinguish individual devices that share the same signing authority
- Add
- Cli
- Automatic detection of the authorized tenant is added.
- The
delete tenantcommand allows cascading deletion of users and access passes.
- Telemetry
- extend device telemetry agent to measure RTT to child geoProbes via TWAMP, generate signed LocationOffset structures, and deliver them via UDP as per rfcs/rfc16-geolocation-verification.md
- geoprobe-target: example target listener for geolocation verification with TWAMP reflector, UDP offset receiver, signature chain verification, distance calculation logging, and DoS protections (5-reference depth limit and per-source-IP rate limiting) (#2901)
- Onchain programs
- feat(serviceability): add TenantBillingConfig and epoch tracking to UpdatePaymentStatus (#2922)
- feat(smartcontract): add payment_status, token_account fields and UpdatePaymentStatus instruction (#2880)
- fix(smartcontract): correctly ser/deser ops_manager_pk (#2887)
- Serviceability: add metro_routing and route_liveness boolean fields to Tenant for routing configuration
- Serviceability: add Tenant account type with immutable code-based PDA derivation, VRF ID, administrator management, and reference counting for safe deletion
- Serviceability: add TenantAddAdministrator and TenantRemoveAdministrator instructions for foundation-managed administrator lists
- Serviceability: extend UserUpdate instruction to support tenant_pk field updates with automatic reference count management on old and new tenants (backward compatible with old format)
- Serviceability: extend UserCloseAccount instruction to decrement tenant reference count when closing user with assigned tenant
- Serviceability: add reference count validation in DeleteMulticastGroup to prevent deletion when active publishers or subscribers exist
- Serviceability: fix multicast group closeaccount to use InvalidStatus error and remove redundant publisher/subscriber count check
- Serviceability: add tenant_allowlist field to AccessPass to restrict which tenants can use specific access passes (backward compatible with existing accounts)
- Serviceability: bypass validation for link delete (#2934)
- Serviceability: add per-device unicast and multicast user limits with separate counters and configurable max values (RFC-14)
- Fix link device & link updates
- SDK
- Add metro_routing and route_liveness fields to CreateTenantCommand and UpdateTenantCommand
- Add CreateTenant, UpdateTenant (vrf_id only), DeleteTenant, GetTenant, and ListTenant commands with support for code or pubkey lookup
- Add AddAdministratorTenant and RemoveAdministratorTenant commands for tenant administrator management
- UpdateUserCommand extended with tenant_pk field and automatic tenant account resolution for reference counting
- SetAccessPassCommand extended with tenant field to specify allowed tenant for access pass
- TypeScript SDK updated with tenantAllowlist field in AccessPass interface and deserialization
- CLI
- Fix
multicastgroup updatecommand to properly parse human-readable bandwidth values (e.g., "1Gbps", "100Mbps") in--max-bandwidthflag - Add --metro-route and --route-aliveness flags to tenant create and update commands
- Add tenant subcommands (create, update, delete, get, list, add-administrator, remove-administrator) to doublezero and doublezero-admin CLIs
- Support simultaneous publisher and subscriber multicast via
--publishand--subscribeflags - Add
--max-unicast-usersand--max-multicast-usersflags todevice updatecommand - Add filtering options and desired_status & metrics_publisher_pk field to device and link list commands
- Added activation check for existing users before subscribing to new groups (#2782)
- access-pass set: add --tenant argument to specify tenant code for access pass restriction (converts to tenant PDA onchain)
- tenant list: improve output formatting with table support and JSON serialization options (--json, --json-compact)
- default tenant support added to config
- Fix
- SDK
- Add read-only Go SDK (
revdist) for the revenue distribution Solana program, with typed deserialization of all onchain accounts and Rust-generated fixture tests for cross-language compatibility - Add
revdist-clitool for inspecting onchain revenue distribution state - Add Python and TypeScript SDKs for serviceability, telemetry, and revdist programs with typed deserialization, RPC clients, PDA derivation, enum string types, and cross-language fixture tests
- Add shared
borsh-incrementallibrary (Go, Python, TypeScript) for cursor-based Borsh deserialization with backward-compatible trailing field defaults - Add npm and PyPI publish workflows for serviceability and telemetry SDKs
- DeleteUserCommand updated to wait for activator to process multicast user unsubscribe before deleting the user
- Add read-only Go SDK (
- Device controller
- Record successful GetConfig gRPC calls to ClickHouse for device telemetry tracking
- Multi-tenancy vrf support added
- Skip isis and pim config for CYOA/DIA tagged interfaces
- Onchain programs
- Enforce that
CloseAccessPassonly closes AccessPass accounts whenconnection_count == 0, preventing closure while active connections are present.
- Enforce that
- Monitor
- Add sol-balance watcher to track SOL balances for configured accounts and export Prometheus metrics for alerting
- Client
- Support simultaneous publisher and subscriber multicast in the daemon
- Telemetry
- Add consecutive-loss-based sender eviction to the telemetry collector so broken TWAMP senders are recreated quickly instead of persisting until TTL expiry (
--max-consecutive-sender-losses, default 30)
- Add consecutive-loss-based sender eviction to the telemetry collector so broken TWAMP senders are recreated quickly instead of persisting until TTL expiry (
- E2E tests
- e2e: add multi-tenancy VRF isolation test (#2891)
- Add backward compatibility test that validates older CLI versions against the current onchain program by cloning live state from testnet and mainnet-beta
- QA multicast tests: add diagnostic dumps on failure (status, routes, latency, multicast reports, onchain user/device state), cleanup stale test groups at test start, and fix disconnect blocking on stuck daemon status
v0.8.6 – 2026-02-04
- None for this release
- CLI
- Remove log noise on resolve route
doublezero resource verifycommand added to verify onchain resources- Enhance delete multicast group command to cascade into deleting AP entry (#2754)
- Onchain programs
- Removed device and user allowlist functionality, updating the global state, initialization flow, tests, and processors accordingly, and cleaning up unused account checks.
- Serviceability: require DeactivateMulticastGroup to only close multicast group accounts when both
publisher_countandsubscriber_countare zero, preventing deletion of groups that still have active publishers or subscribers. - Deprecated the user suspend status, as it is no longer used.
- Serviceability: enforce that CloseAccountUser instructions verify the target user has no multicast publishers or subscribers (both
publishersandsubscribersare empty) before closing, and add regression coverage for this behavior. - Enhance access pass functionality with new Solana-specific types
- fix default desired status
- Telemetry
- Fix goroutine leak in TWAMP sender —
cleanUpReceivedgoroutines now exit onClose()instead of living until process shutdown
- Fix goroutine leak in TWAMP sender —
- Client
- Cache network interface index/name lookups in liveness UDP service to fix high CPU usage caused by per-packet RTM_GETLINK netlink dumps
- Add observability to BGP handleUpdate: log withdrawal/NLRI counts per batch and track processing duration via
doublezero_bgp_handle_update_duration_secondshistogram
- E2E tests
v0.8.5 – 2026-02-02
- None for this release
- Smartcontract
- fix(smartcontract): reserve first IP of DzPrefixBlock for device (#2753)
- Client
- Fix race in bgp status handling on peer deletion
v0.8.4 – 2026-01-28
- None for this release
- Telemetry
- Force IPv4-only connections for gNMI tunnel client and fix TLS credential handling
- Client
- Support simultaneous unicast and multicast tunnels in doublezerod
- Support publishing and subscribing to multiple multicast groups simultaneously
- CLI
- Support publishing and subscribing a user to multiple multicast groups via
--groupflag - Remove single tunnel constraint
- Support publishing and subscribing a user to multiple multicast groups via
- SDK
- Go SDK can now perform batch writes to device.health and link.health as per rfc12
- Activator
- fix(activator): add on-chain allocation support for users (#2744)
- On-chain allocation enabled
- Smartcontract
- feat(smartcontract): add use_onchain_deallocation flag to MulticastGroup (#2748)
- CLI
- Remove restriction for a single tunnel per user; now a user can have a unicast and multicast tunnel concurrently (but can only be a publisher or a subscriber) (2728)
v0.8.3 – 2026-01-22
- Data
- Add indexer that syncs serviceability and telemetry data to ClickHouse and Neo4J
- None for this release
- CLI
- Remove log noise on resolve route
- Add
global-config qa-allowlistcommands to manage QA identity allowlist to bypass status and max_users checks in QA - Add "-skip-capacity-check" flag to bypass status and max_users checks in QA to test devices that are still being provisioned
- Remove "unknown" status from doublezero status command and implement "failed" and "unreachable" statuses
- Client
- Enable route liveness passive-mode by default
- Add
make installmake target. To build and deploy from source, users can now runcd client && make build && make installto install the doublezero and doublezerod binaries and the doublezerod systemd unit.
- Onchain programs
- Serviceability: remove validation check for interface delete (#2707)
- Serviceability: interface-cyoa only on physical interfaces, don't require interfaces to be tagged, add same validation logic to update interface (#2700)
- Enforce Activated status check before suspending contributor, exchange, location, and multicastgroup accounts
- Removed device and user allowlist functionality, updating the global state, initialization flow, tests, and processors accordingly, and cleaning up unused account checks.
- Serviceability: require DeactivateMulticastGroup to only close multicast group accounts when both
publisher_countandsubscriber_countare zero, preventing deletion of groups that still have active publishers or subscribers. - Deprecated the user suspend status, as it is no longer used.
- Serviceability: enforce that CloseAccountUser instructions verify the target user has no multicast publishers or subscribers (both
publishersandsubscribersare empty) before closing, and add regression coverage for this behavior. - Removed device and user allowlist functionality, updating the global state, initialization flow, tests, and processors accordingly, and cleaning up unused account checks.
- SetGlobalConfig, ActivateDevice, UpdateDevice and CloseAccountDevice instructions updated to manage resource accounts.
- Add option for Contributor B to reject a link created by Contributor A just as Contributor A can cancel its own created link
- Telemetry
- Add gNMI tunnel client for state collection
- Activator
- fix(activator): ip_to_index fn honors ip range #2658
- E2E tests
- Add influxdb, prometheus, and device-health-oracle containers
- Add interface lifecycle tests (#2700)
- Only fail QA alldevices test run if device status is "Activated" and max users > 0
- SDK
- Commands for setting global config, activating devices, updating devices, and closing device accounts now manage resource accounts.
- Serviceability: return error when GetProgramAccounts returns empty result instead of silently returning empty data
- Smartcontract
- feat(smartcontract): RFC 11 activation for User entity
- feat(smartcontract): RFC 11 add on-chain resource allocation for Link
- Device Health Oracle
- Add new device-health-oracle component. See rfcs/rfc12-network-provisioning.md for details.
- Calculate burn-in timestamp based from slot numbers (current minus 200_000 slots for provisioning, current minus 5_000 slots for maintenance)
- CI
- Add separate apt repo for doublezero-testnet
v0.8.2 – 2025-01-13
- None for this release
- Client
- Always delegate RouteAdd regardless of noUninstall flag
- Telemetry
- Include solana vote pubkey in global monitor metrics
- Run telemetry agent on pending and drained links
v0.8.1 – 2025-01-12
- Onchain programs
- Serviceability: enforce that ActivateLink and CloseAccountLink instructions verify the provided side A/Z device accounts match the link's stored
side_a_pkandside_z_pkbefore proceeding.
- Serviceability: enforce that ActivateLink and CloseAccountLink instructions verify the provided side A/Z device accounts match the link's stored
- CLI
- Update contributor, device, exchange, link, location, and multicast group commands to ignore case when matching codes
- ActivateMulticastGroup now supports on-chain IP allocation from ResourceExtension bitmap (RFC 11).
- IP address lookup responses that do not contain a valid IPv4 address (such as upstream timeout messages) are now treated as retryable errors instead of being parsed as IPs.
doublezero resourcecommands added for managing ResourceExtension accounts.- Added health_oracle to the smart contract global configuration to manage and authorize health-related operations.
- Added --ip-net support to create to match the existing behavior in update.
- Use DZ IP for user lookup during status command instead of client IP
- Onchain programs
- Fix CreateMulticastGroup to use incremented globalstate.account_index for PDA derivation instead of client-provided index, to ensure the contract is the authoritative source for account indices
- Add on-chain validation to reject CloseAccountDevice when device has active references (reference_count > 0)
- Allow contributor owner to update ops manager key
- Add new arguments on create interface cli command
- Serviceability: enforce that resume instructions for locations, exchanges, contributors, devices, links, and users only succeed when the account status is
Suspended, returningInvalidStatusotherwise, and add tests to cover the new behavior. - RequestBanUser: only allow requests when user.status is Activated or Suspended; otherwise return InvalidStatus
- Serviceability: require device interfaces to be in
Pendingstatus before they can be rejected, and add tests to cover the new status check - Add ResourceExtension to track IP/ID allocations. Foundation instructions added to create/allocate/deallocate.
- ResourceExtension optimization using first_free_index for searching bitmaps
- Added the INSTRUCTION_GUIDELINES document defining the standard for instruction creation.
- Enforce best practices for instruction implementation across onchain programs
- Add missing system program account owner checks in multiple instructions
- Refactor codebase for improved maintainability and future development
- Introduced health management for Devices and Links, adding explicit health states, authorized health updates, and related state, processor, and test enhancements.
- Require that BanUser can only be executed when the target user's status is PendingBan, enforcing the expected user ban workflow (request-ban -> ban).
- Introduce desired status to Link and Devices
- Introduced health management for Devices and Links, adding explicit health states, authorized health updates, and related state, processor, and test enhancements.
- Restrict DeleteDeviceInterface to interfaces in Activated or Unlinked status; attempting to delete interfaces in other statuses now fails with InvalidStatus.
- Updated validation to allow public IP prefixes for CYOA/DIA, removing the restriction imposed by type-based checks.
- Transit devices can now be provisioned without a public IP, aligning the requirements with their actual networking model and avoiding unnecessary configuration constraints.
- Enforce that ActivateDeviceInterface only activates interfaces in Pending or Unlinked status, returning InvalidStatus for all other interface states
- Introduce desired status to Link and Devices
- Internet Latency Telemetry
- Fixed a bug that prevented unresponsive ripeatlas probes from being replaced
- Fixed a bug that caused ripeatlas samples to be dropped when they were delayed to the next collection cycle
- Link & device Latency Telemetry
- Telemetry data can now be received while entities are in provisioning and draining states.
- Device controller
- Add histogram metric for GetConfig request duration
- Add gRPC middleware for prometheus metrics
- Add device status label to controller_grpc_getconfig_requests_total metric
- Add logic to shutdown user BGP, IBGP sessions, MSDP neighbors, and ISIS when device.status is drained
- Device agents
- Increase default controller request timeout in config agent
- Initial state collect in telemetry agent
- Client
- Route liveness treats peers that advertise passive mode as selectively passive; does not manage their routes directly.
- Route liveness runs in passive mode for IBRL with allocated IP, if global passive mode is enabled.
- Advertise peer client version with route liveness control packets.
- Add
doublezero_bgp_routes_installedgauge metric for number of installed BGP routes - Add route liveness gauges for in-memory maps
- Route liveness sets set of routes configured as excluded to
AdminDown. - Add histogram metric for BGP session establishment duration
- For IBRL with allocated IP mode, resolve tunnel source IP from routing table via resolve-route API endpoint instead of using client IP to support clients behind NAT
- Configure MTU down to 1476 on client tunnel in case path MTU discovery is not working
- Increase route liveness max backoff duration
- Global monitor
- Initial implementation
- Release
- Publish a Docker image for core components.
- Telemetry
- Refactor flow enricher
- Add metrics to flow enricher
- Add serviceability data fetching to flow enricher
- Add flow-ingest service
- Add annotation of flow records with serviceability data
- Add pcap input and json ouput to flow enricher
- Initial state-ingest service with client SDK
- Collect BGP socket state from devices
- CI
- Cancel existing e2e test runs on the push of new commits
- RFCs
- RFC - Network Provisioning
- RFC-11: Onchain Activation (#2302)
- Monitor
- Add link status to device-telemetry metrics to enable Grafana alerts to filter out links that are not in activated status
- Add validation for 2Z oracle swapRate to ensure it is an unsigned integer, with warning logs and metrics for malformed values
- E2E tests
- Add GetLatency call to qaagent
- The QA alldevices test now considers device location and connects hosts to nearby devices
- QA agent and tests now support doublezero connect ibrl's --allocate-addr flag
- The QA alldevices test now publishes success/failure metrics to InfluxDB in support of rfc12
- Onchain programs
- Fix CreateMulticastGroup to use incremented globalstate.account_index for PDA derivation instead of client-provided index, to ensure the contract is the authoritative source for account indices
- ReactivateMulticastGroup now enforces that the multicast group status must be Suspended before reactivation, returning InvalidStatus otherwise; negative-path regression tests were added.
v0.8.0 – 2025-12-02
- None for this release
- RFCs
- RFC-10: Version Compatibility Windows
- CLI
- IP address lookups via ifconfig.me are retried up to 3 times to minimize transient network errors.
- Added global
--no-version-warningflag to thedoublezeroclient and now emit version warnings to STDERR instead of STDOUT to improve scriptability and logging. - Add the ability to update a Device’s location, managing the reference counters accordingly.
- Added support in the link update command to set a link’s status to soft_drained or hard_drained.
- Added support for specifying
device_typeat creation, updating it via device update, and displaying it in list/detail outputs. - Add support for updating
contributor.ops_manager_key. - Add migrate command to upgrade legacy user accounts from index-based PDAs to the new IP + connection-type scheme.
- Enhance
access-pass listwith client-IP and user-payer filters - Support added to load keypair from stdin
- Client
- Add route liveness fault-injection simulation tests.
- Updated the
interface listcommand to display all interfaces when no device is specified.
- Funder
- Fund multicast group owners
- Onchain programs
- Serviceability Program: Updated the device update command to allow modifying a device’s location.
- Added new
soft-drainedandhard-drainedlink status values to serviceability to support traffic offloading as defined in RFC9. - Fix ProgramConfig resize during global state initialization.
- Standardized the
device_typeenum toEdge,Transit, andHybrid, added validation rules, and defaulted existing devices toHybridfor backward compatibility. - Add
contributor.ops_manager_keyfor authorizing incident management operations. - User Account: Replace global-index PDA generation with deterministic IP + connection-type seeds, eliminating user-creation race conditions.
- Enable on-chain storage of InterfaceV2, allowing devices to register updated interface metadata
- Serviceability: validate that a device's public IP doesn't clash with its dz_prefixes
- QA
- Traceroute when packet loss is detected
- Tools
- Add
solana-tpu-quic-pingtool for testing Solana TPU-QUIC connections with stats emitted periodically
- Add
- Device controller
- Handle new link.status values (soft-drained and hard-drained) as per RFC9
- Monitor
- Export links data to InfluxDB
- Activator
- Uses asynchronous coroutines instead of blocking operations and threads.
v0.7.1 – 2025-11-18
- None for this release
- RFCs
- RFC9 Link Draining
- Client
- Switch to 64 byte latency probes instead of 32 bytes
- Route liveness admin-down signalling and ignore stale remote-down messages
- Added an on-chain minimum supported CLI version to allow multiple CLI versions to operate simultaneously.
- Smart contract
- Delay V2 Interface Activation Until All Clients Support V2 Reading
- Device controller
- Now accepts the config agent's version in the grpc GetConfig call and includes it as a label in the controller_grpc_getconfig_requests_total metric
- Device Agent
- Now sends its version to the controller in the grpc GetConfig call
v0.7.0 – 2025-11-14
- Smart contract
- Introduces CYOA and DIA as new possible interface types
- Onchain programs
- Check if
accesspass.owneris equal to system program (malbeclabs/doublezero#2088)
- Check if
- CLI
- Added support for specifying the interface type during interface creation and modification, introducing CYOA and DIA as new possible interface types.
- Improve error message when connecting to a device that is at capacity or has max_users=0. Users now receive "Device is not accepting more users (at capacity or max_users=0)" instead of the confusing "Device not found" error when explicitly specifying an ineligible device.
- Add
link latencycommand to display latency statistics from the telemetry program. Supports filtering by percentile (p50, p90, p95, p99, mean, min, max, stddev, all), querying by link code or all links, and filtering by epoch. Resolves: #1942 - Added
--contributor | -cfilter todevice list,interface list, andlink listcommands. (#1274) - Validate AccessPass before client connection (#1356)
- Client
- Add initial route liveness probing, initially disabled for rollout
- Add route liveness prometheus metrics
v0.6.11 – 2025-11-13
- None for this release
- Note that the changes from this release have been bundled into 0.7.0
v0.6.10 – 2025-11-05
- None for this release
- CI
- Add automated compatibility tests in CI to validate all actual testnet and mainnet state against the current codebase, ensuring backward compatibility across protocol versions.
- Add
--delay-override-msoption todoublezero link update - Add ability to configure excluded routes
- Device controller
- Remove the deprecated -enable-interfaces-and-peers flag
- Use link.delay_override to set isis metric when in valid range. This provides a simple workflow for contributors to temporarily change a link's delay value without overwriting the existing value.
- Onchain programs
- serviceability: add delay_override_ns field to link
v0.6.9 – 2025-10-24
- None for this release
- Onchain programs
- serviceability: add auto-assignment and validation for exchange.bgp_community
- serviceability: prevent device interface name duplication
- Update serviceability and telemetry program instruction args to use the
BorshDeserializeIncrementalderive macro incremental, backward-compatible, deserialization of structs. - Add explicit signer checks for payer accounts across various processors to improve security and ensure correct transaction authorization.
- CLI
- Removed
--bgp-communityoption fromdoublezero exchange createsince these values are now assigned automatically - Add
--next-bgp-communityoption todoublezero global-config setso authorized users can control which bgp_community will be assigned next
- Removed
- Tools
- TWAMP: Verify that the sequence number and timestamp of the received packet matches those of the sent packet
- Uping: Add minimal ICMP echo library for user-space liveness probing over doublezero interfaces, even when certain routes are not in the the kernel routing table.
- Device controller
- Deprecate the -enable-interfaces-and-peers flag. The controller now always renders interfaces and peers
- Intra-exchange routing policy, which uses the onchain exchange.bgp_community value to route traffic between users in the local exchange over the internet
- Monitor
- Add metrics that detect when duplicate or out-of-range exchange.bgp_community values exist in serviceability
v0.6.8 – 2025-10-17
- Multicast group change: Regeneration of all multicast group allowlists required, as allowlists are now stored within each Access Pass instead of at the multicast group level.
- Onchain programs
- Serviceability: enforce that ActivateLink and CloseAccountLink instructions verify the provided side A/Z device accounts match the link's stored
side_a_pkandside_z_pkbefore proceeding.
- Serviceability: enforce that ActivateLink and CloseAccountLink instructions verify the provided side A/Z device accounts match the link's stored
- CLI
- Added a wait in the
disconnectcommand to ensure the account is fully closed before returning, preventing failures during rapid disconnect/reconnect sequences. - Display multicast group memberships (publisher/subscriber) in AccessPass listings to improve visibility.
- Allow AccessPass creation without 'client_ip'
- Add 'allow_multiple_ip' argument to support AccessPass connections from multiple IPs
- Include validator pubkey in
exportoutput - Rename exchange.loc_id to exchange.bgp_community
statuscommand now shows connected and lowest latency DZD
- Added a wait in the
- Activator
- Reduce logging noise when processing snapshot events
- Wrap main select handler in loop to avoid shutdown on branch error
- Onchain programs
- Remove user-level allowlist management from CLI and admin interfaces; manage multicast group allowlists through AccessPass.
- Add Validate trait for core types (AccessPass, Contributor, Interface, etc.) and enforce runtime checks before account operations.
- Fix: resize AccessPass account before serialization to prevent errors; standardized use of resize_account_if_needed across processors.
- Enable AccessPass with 'client_ip=0.0.0.0' to dynamically learn the user’s IP on first connection
- Enable AccessPass to support connections from multiple IPs (allowlist compatibility)
- Rename exchange.loc_id to exchange.bgp_community, and change it from u32 to u16
- Internet telemetry
- Add circuit label to metrics; create a new metric for missing circuit samples
- Create a new metric that tracks how long it takes collector tasks to run
- Submit partitions of samples in parallel
- Include circuit label on submitter error metric
- Monitor
- Reduce logging noise in 2z oracle watcher
- Include response body on 2z oracle errors
- Collect contributors and exchanges into InfluxDB
- Device controller
- When a device is missing required loopback interfaces, provide detailed errors to agent instead of " not found". Also, log these conditions as warnings instead of errors, and don't emit "unknown pubkey requested" error metrics for these conditions
- Add device info as labels to
controller_grpc_getconfig_requests_totalmetric
- Device agents
- Submit device-link telemetry partitions in parallel
- CLI
- Allow AccessPass creation without 'client_ip'
- Add 'allow_multiple_ip' argument to support AccessPass connections from multiple IPs
- Rename exchange.loc_id to exchange.bgp_community
- Onchain programs
- Enable AccessPass with 'client_ip=0.0.0.0' to dynamically learn the user’s IP on first connection
- Enable AccessPass to support connections from multiple IPs (allowlist compatibility)
- Rename exchange.loc_id to exchange.bgp_community, and change it from u32 to u16
- Telemetry data API
- Filter by contributor and link type
- SDK/Go
- String serialization for exchanges status
- Exclude empty tags from influx serialization
v0.6.6 – 2025-09-26
- None for this release
- Monitor
- Update 2Z oracle to emit response code metrics on errors too
- Activator
- A mitigation was added to handle situations where blockchain updates are missed by the Activator due to timeouts on the websocket. This mitigation processes pending accounts on a 1-minute timer interval.
- CLI
- Connect command updated to provide better user experience with regard to activator websocket timeouts (see above).
v0.6.5 – 2025-09-25
- None for this release
- CLI
- Connect now waits for doublezerod to get all latencies
- Latency command sorts unreachable to bottom
- Device controller
- Update device template to set default BGP timers and admin distance
- Update device template so all "default interface TunnelXXX" commands for user tunnels come before any other user tunnel config
- Activator
- Fix access pass check status accounts list
- Onchain programs
- Implemented strict validation to ensure that only AccessPass accounts owned by the program and of the correct type can be closed.
- Fix Access Pass set Instruction.
- Switched to using
account_closehelper for closing accounts instead of resizing and serializing. - Make interface name comparison case insensitive
- Onchain monitor
- Check for unlinked interfaces in a link
- Emit user events
- Add watcher for 2Z/SOL swap oracle
v0.6.4 – 2025-09-10
- None for this release
- Onchain programs
- Fix bug preventing re-opening of AccessPass after closure
- sc/svc: guard against empty account data
- Device controller
- Support server dual listening on TLS and non-TLS ports
- Device and Internet Latency Telemetry
- Create one ripeatlas measurement per exchange instead of per exchange pair to avoid concurrent measurement limit
v0.6.3 – 2025-09-08
- None for this release
- Onchain programs
- Expand DoubleZeroError with granular variants (invalid IPs, ASN, MTU, VLAN, etc.) and derive PartialEq for easier testing.
- Rename Config account type to GlobalConfig for clarity and consistency.
- Fix bug in user update that caused DZ IP to be 0.0.0.0
- Add more descriptive error logging
- Telemetry program: embed serviceability program ID via build feature instead of env variable
- Activator
- Support for interface IP reclamation
- Devices are now initialized with max_users = 0 by default.
- Devices with max_users = 0 cannot accept user connections until updated.
- Onchain monitor
- Emit metric for telemetry account not found in device and internet telemetry watchers
- Emit metric with serviceability program onchain version
- Delete telemetry counter metrics if circuit was deleted
- Telemetry
- Fix dashboard API to handle partitioned query with no samples
- Add summary view with committed RTT and jitter, compared to measured values
- Device agents
- Remove log of keypair path on telemetry agent start up
- Drop device telemetry samples if submission attempts exhausted and buffer is at capacity
- Device controller
- Each environment can now have a different device BGP Autonomous System Number (ASN) per environment. (This is the remote ASN from the client's perspective.)
- Add flag for enabling pprof for runtime profiling
- E2E tests
- Updated unit tests and e2e tests to validate the new initialization and activation flow.
- Contributor Operations
- Contributors must explicitly run device update to set a valid max_users and activate a Device.
v0.6.2 – 2025-09-02
- None for this release
- Onchain programs
- Fix: Serviceability now correctly enforces device.max_users
- Fix: Restored the
validator_pubkeyfield from AccessPass. This field had been removed in the previous version but is required by Sentinel. - Fix: Skip client version check in
statuscommand to prevent version errors during automated state checks. - New instructions were added to support device interface create/update/delete that prevents a race condition that could cause some updates to be lost when changes were made in quick succession.
- Add deserialization vector with capacity + 1 to avoid
memory allocation failed, out of memoryerror
- CLI
- Added filtering options to
access-pass listanduser listCLI commands. - New filters include access pass type (
prepaidorsolana-validator) and Solana identity public key. - Updated command arguments and logic, with tests adjusted to cover new options.
- Contributors: Interface creation no longer takes an "interface type (physical/loopback)" argument. The type is now inferred from the interface name.
- Added filtering options to
- Device controller
- Use serviceability onchain delay for link metrics
v0.6.0 – 2025-08-28
- Onchain programs
- Implement access pass management commands and global state authority updates
- Update access pass PDA function to include payer parameter
- Onchain programs
- Introducing new link instruction processor acceptance criteria
- Add support for custom deserializers and add for pubkey fields
- Move serialization and network_v4 to program-common
- Refactor account type assertions in processors and state modules in serviceability program
- Add validator identity to
SolanaValidatortype AccessPass.
- User client
- Add access pass management commands to CLI
- Restructuring device and global config CLI commands for better authority and interface management
- Enhance the handling and display of access pass epoch information in the CLI
- Configure CLI network settings with shorthand network code. Usage:
doublezero config set --env <testnet|mainnet-beta> - Configure
doublezerodnetwork settings with shorthand network code. Usagedoublezerod --env <testnet|mainnet-beta> - Add associated AccessPass to user commands.
- Activator
- Introduce new user monitoring thread in activator for access pass functionality
- Remove validator verification via gossip. This functionality is migrated to AccessPass.
- Device controller
- Implement user tunnel ACLs in device agent configuration
- Add "mpls icmp ttl-exceeded tunneling" config statement so intermediate hops in the doublezero network respond to traceroutes.
- Set protocol timers for ibgp and isis to improve to speed up network re-convergence
- Add TLS support to gRPC server
- Onchain monitor
- Initial implementation and component release
- Monitor onchain device telemetry metrics
- Monitor onchain internet latency metrics
- E2E tests
- Simplify fixtures with loop rollups
- Add user ban workflow test
- Deflake user reconnect race and device interface assigned IP race
- Add single device stress test
- Adjust user validation commands to use the new AccessPass column.
- CLI
- Refactor: Updated
SetAccessPassCliCommand(doublezero access-pass set) to use--epochsinstead of--last_access_epoch, with sensible default values. - AccessPass now requires passing the validator identity for the
SolanaValidatortype.
- Refactor: Updated
- Device Agents
- Periodically recreate telemetry agent sender instances in case of interface reconfiguration.
- Telemetry
- Optimize onchain data dashboard API responses with field filtering
- Optimize onchain data data CLI execution with parallel queries
- Dashboard API support for circuit set partitioning using query parameters
v0.5.3 – 2025-08-19
- CLI & UX Improvements
- Improve sorting of device, exchange, link, location, and user displays
- New installation package for the admin CLI for contributors based on controller/doublezero-admin
- Do not allow users to connect to a device with zero available tunnel slots remaining
- Improve handling of interface names for
doublezero device interfacecommands
- Serviceability Model Improvements
- funder: configure recipients as flag
- sdk/rs: add record program handling
- config: use ledger RPC LB endpoint
- Validate account codes and replace whitespace
- config: add ability to override DZ ledger RPC url; update URLs
- Remove old CloseAccount instruction from both the smart contract and SDK client code
- Network Controller Improvements
- Increase user tunnel slots per device from 64 to 128
- Add flag controlling whether interfaces and peers are rendered to assist with testnet migration
- Device and Internet Latency Telemetry
- Internet latency samples in data CLI and dashboard API
- internet-latency-collector, telemetry data api/cli: collect internet latency between exchanges, not locations
- internet-latency-collector: add ripeatlas credit metric
- End-to-End Tooling
- New doublezero QA agent improves quality by thoroughly testing the software stack end-to-end in each doublezero environment (devnet, testnet, mainnet-beta) after each release.
v0.5.0 – 2025-08-11
- CLI & UX Improvements
doublezero connectnow waits for the user account to be visible onchain.doublezero device interfacecommands. Interface names get normalized.- General improved consistency
- Easy switching between devnet and testnet using the
--envflag
- Device Latency Telemetry
- Data CLI and API use epoch from ledger
- Backpressure support to avoid continual buffer growth in error conditions
- Link pubkey used for circuit uniqueness
- Internet Latency Telemetry
- Adds the environement (devnet/testnet/mainnet-beta) to the ripeatlas measurement description.
- Now funded by the funder
- Serviceability Model Improvements
- Device extended to add DZD metadata (including Interfaces)
- DZX Link types added (clearly distinguished from WAN links)
- Removed foundation allowlist check, streamlining
linkworkflow - Validate that
link.account_typehas typeAccountType::Link
- Network Controller Improvements
- doublezero-controller now manages more of the DZD configuration, including:
- DNS servers
- NTP servers
- DZ WAN interfaces
- Necessary loopback interfaces
- BGP neighbor configuration
- MSDP configuration
- doublezero-activator now assigns IP addresses for use by the controller to give to DZD wan interfaces as well as loopbacks.
- doublezero-controller now manages more of the DZD configuration, including:
v0.4.0 – 2025-08-04
This release adds contributor ownership, reference counting, and improved CLI outputs for devices and links. It introduces internet latency telemetry, with support for collection, Prometheus metrics, and writing samples to the ledger. Device telemetry now uses ledger epochs for network-wide consistency.
- Serviceability Model Improvements
- Contributor creation includes an
ownerfield; device/link registration enforces contributor consistency - Contributor field shown in CLI
listandgetcommands for devices and links reference_countadded to contributors, devices, locations, and exchanges- New fields added to
DeviceandLink, including aninterfacesarray forDevice - Go SDK updated to support new DZD metadata account layouts
- Contributor creation includes an
- CLI & UX Improvements
- Provisioning (
connect,decommission) UX improved: clearer feedback, better spinners, and more accurate status messages doublezero latencyoutput includes device code alongside pubkeydoublezero deviceanddoublezero linkcommands updated to show new metadata fields- Added
doublezero device interfacesubcommands for managing interfaces keygencommand now supports--outfile(-o) flag to generate keys directly to a file
- Provisioning (
- Device Latency Telemetry
- Agent now uses ledger epoch instead of wallclock-based epoching
- Account layout updated to move
epochafter discriminator for efficient filtering
- Internet Latency Telemetry
- Internet latency collectors write samples to the ledger using epoch-based partitioning
- Telemetry program supports ingesting external control-plane latency samples
- Prometheus metrics expose collector operation, failure rates, and credit balances
- Go SDK support for initializing and submitting latency samples
- End-to-End Tooling
- Multicast monitor utility added for provisioning validation
- Multi-client e2e tests cover IBRL with and without IP allocation
v0.3.0 - 2025-07-28
This release introduces network contributor registration, device interface management, and the initial telemetry system for link latency. Prometheus metrics were added to the activator and client for observability. Provisioning flows now enforce contributor presence and IP uniqueness per user.
- Contributor Support
- Added CLI support for contributor management via
doublezero contributor - Used to register network contributors in the DoubleZero system
- Added CLI support for contributor management via
- Device Interface Management
- Added device interface CRUD commands for managing interfaces on a device
- Interface metadata will be used by the controller to generate device configuration
- Link Telemetry System
- Introduced TWAMP-based telemetry agent and onchain program for measuring RTT and packet loss between devices
- Lays the foundation for performance-based rewards for bandwidth contributors
- Prometheus Metrics
- Activator and client now export Prometheus metrics (build info, BGP session status)
- Provisioning & Decommissioning
- Enforced one tunnel per user per IP address
- Contributor field now required when creating devices and links
- Activator
- Improved metrics and error handling
- Added graceful shutdown and signal handling
- Client
- Added
-jsonoutput flag forstatusandlatencycommands
- Added