Skip to content

v0.7.1

Latest

Choose a tag to compare

@github-actions github-actions released this 04 Nov 17:46
· 45 commits to master since this release
v0.7.1
611b29f

📄 Canonical release notes: docs/release-notes/0.7.1.md

satd 0.7.1

Released 2026-11-03 · git tag v0.7.1 · Changelog

A bug-fix release for 0.7.0. No reindex. One RPC field changes type to match
Core, the AssumeUTXO warnings have new ids, and a few defaults and limits
change; read the Upgrade notes before upgrading.

Highlights

  • Signet and -reindex hold blocks to Core's header rules. A signet
    block must carry the scheduled difficulty, and -reindex checks every
    stored block's header as header sync does, so it neither replays nor
    indexes a block that breaks them (details).
  • Core-compatible RPC. Any request Content-Type is accepted, so Core's
    own curl examples work; warnings is always an array; and
    getnetworkinfo reports -onlynet reachability as Core does
    (details).
  • No freeze on a 1-CPU host, and no slot held by a stalled client. RPC
    and P2P no longer freeze together when one task blocks during initial
    sync, and a client that stops reading is disconnected
    (details, HTTP surfaces).
  • AssumeUTXO background validation retries a failed connect instead of
    stopping, and its alerts reach the status stream, webhooks and metrics as
    well as getwarnings (details).
  • Sparrow batches against satd's Electrum server, and block reads no
    longer wait behind each stored block's fsync (Electrum,
    Storage).
  • Reproducible builds. The Nix build of satd is byte-identical whichever
    x86_64 CPU builds it, and flake.lock is committed (details).

RPC

  • Any request Content-Type is accepted (#824). Bitcoin Core reads the JSON-RPC body whatever the request's Content-Type says. satd added application/json only when the header was missing, so a request that named another type got 415 Unsupported Media Type. That included Core's documented form, -H 'content-type: text/plain;', and the form type plain curl --data sends. Every request is now read as JSON.

  • getrawtransaction's not-found message matches the node's index (#1066). With -txindex on, satd still told the caller to enable -txindex. The message now depends on the index, as Core's does:

    • With no index, it keeps that advice.
    • With a complete index, it says No such mempool or blockchain transaction.
    • With an index that was turned on after blocks were already connected, it says those blocks are not covered and need a block hash. satd never backfills the index. This stands in for Core's "still being indexed" case.

    The error code is still -5.

  • testmempoolaccept agrees with sendrawtransaction under an allow policy rule (#999). With a -policyfile that holds an allow rule, admission records a failure in the exemptable standardness set (too many dust outputs, a non-standard output or input script, ephemeral dust that pays a fee) and forgives it when the transaction's verdict is Allow. testmempoolaccept returned the standardness refusal before it ran the policy, so it refused transactions sendrawtransaction accepted. It now records the same failures, runs the policy dry run, and forgives them on an Allow verdict. A Pass or Quarantine verdict still refuses with the standardness reason, and a node without an allow rule is unchanged.

  • getnetworkinfo reports which networks are reachable (#1164). ipv4 and ipv6 were always reachable: true, limited: false, so a node under -onlynet=onion told its operator that clearnet was in use when it made no clearnet connections. Each network's flags now come from the set that drives outbound connections, as Core's g_reachable_nets does: -onlynet decides, and onion also needs a proxy that reaches Tor and no -onion=0. networks now lists Core's five networks: i2p is never reachable, because satd has no I2P transport, and cjdns is reachable only with -cjdnsreachable.

  • warnings is always an array (#956). getblockchaininfo, getnetworkinfo and getmininginfo returned "warnings": "" on a node with no warnings and an array of strings otherwise, so the field's type depended on the node's state. It is now always an array, [] when there are none, as in Core since v28. The new -deprecatedrpc=warnings brings back Core's deprecated form: one string, the latest warning, or "". -deprecatedrpc with any other name is warned about at startup and ignored.

  • The capability check comes before parameter checks (#826). On a listener with -rpcauthbearer, the named-parameter rewrite ran before the capability filter, so a token without rpc:submit that called sendrawtransaction with an unknown named parameter got -8 Unknown named parameter instead of the capability error. The method filters (capabilities, the read-only listener's method list and the startup listener's -28) now run first, as Core's -rpcwhitelist check does, and the caller gets -32004. A call the token may make has its parameters checked as before.

  • An open WebSocket no longer outlives its bearer token (#821). A WebSocket on the JSON-RPC listener resolved its bearer token once, at the upgrade, and jsonrpsee copies that request onto every frame, so the token's principal rode every call for as long as the socket stayed open. Removing the token from the -authfile and sending SIGHUP, or letting its expires pass, did not stop a socket that was already open from calling what the token had been granted, sendrawtransaction included. Each frame now resolves the token again, by its digest, against the token table as it stands. A frame whose token was removed or has expired is answered with error -32006 (bearer token revoked or expired), its notifications are dropped, and a subscribemempool subscription opened with the token ends at its next event. A reload that changes a token's capabilities or rate limit now reaches open sockets too. New connections were already refused with 401.

Electrum

  • Sparrow batches its requests against satd (#1069). Sparrow decides from the server.version name whether a server can take batched requests, and batches only for names it knows, such as one containing electrumx. satd's default name, satd-electrs-compatible/<version>, matched none of them, so Sparrow sent one request per script hash and wallet scans were much slower than they needed to be. The default is now satd-electrs-electrumx-compatible/<version>, which Sparrow recognises. Cake Wallet still finds the electrs token it looks for before probing silent-payment tweaks, and the name still does not start with electrs, which would turn BlueWallet's batching off.
  • -electrummaxbatchrequests defaults to 345 (#1069). Once Sparrow batches, it sends up to 250 blockchain.transaction.get_merkle calls in one batch when it verifies a wallet's transactions, and the old cap of 100 would refuse that batch whole. 345 is Fulcrum's default.

Mempool and relay

  • Transaction versions outside 1 to 3 are non-standard (#1070). Core's IsStandardTx refuses a version below 1 or above 3 with reason version. satd had no version check, so sendrawtransaction, testmempoolaccept and P2P relay accepted a version 0 or version 4 transaction that Core refuses, and satd then relayed it to peers that would drop it. -acceptnonstdtxn lifts the rule on test chains, as in Core.

P2P

  • Disconnect log lines give the right reason (#1152). Every disconnect the node made for its own reasons, such as a stalling block download or misbehaviour, was logged as Disconnecting peer on request ... reason="rpc_request", as if something had called disconnectnode. The line now reads Disconnecting peer. It has reason="rpc_request" only for disconnectnode, and reason="internal" otherwise, next to the line that already gives the cause.

  • Onion peers are logged by their onion address (#1165). The Peer connected and Peer disconnected lines, and the IBD silent-peer warning, printed 0.0.0.0:<port> for a .onion peer. They now print the address getpeerinfo reports, <host>.onion:<port>.

  • A pruned node prunes during initial sync (#1154). The download scheduler shuffled every height up to the header tip into one pool, and its only limit was a count: no more than -maxahead blocks downloaded but not yet connected. Once the window filled, each slot it freed went to a random height anywhere up to the header tip. So every block file held a sprinkling of blocks tens of thousands of heights above the tip. Pruning deletes a file only once its highest block is below the cut, so on a pruned node nothing was deleted until IBD was nearly over, and a small disk filled. Blocks are now handed out in height order and never more than -maxahead above the tip, as Core's download window works. Two smaller paths are closed as well:

    • A block announced by inv during IBD is answered with getheaders only, as in Core, instead of being fetched and stored hours before it can connect.
    • A block nobody asked for is not stored when it lies above the window, as Core's AcceptBlock refuses one too far ahead.

    Blocks stored above the window by 0.7.0 stay until the tip reaches them.

  • Onion seeds for every network (#1162). satd had no .onion fixed seeds for testnet4 or testnet3, so a Tor-only node (-onlynet=onion with -proxy) there had no address to dial unless the operator added one by hand. Its 20 mainnet onion seeds were down to 7 that still answered. Of its 2 signet entries, one was not a valid Tor v3 address. The lists now hold the Tor v3 entries of Bitcoin Core v31.2's fixed seeds, kept only where the node answered a version handshake over Tor with the right network magic and NODE_NETWORK when they were imported: 138 for mainnet, 41 for testnet3, 49 for testnet4 and 48 for signet.

HTTP surfaces

  • A client that stops reading gives its slot back (#823). The JSON-RPC, Esplora and streamws listeners bounded the wait for a request and the idle gap between requests, but not a write. A client that asked for a large response, or opened an SSE stream or WebSocket, and then stopped reading kept its socket, and its slot under the listener's cap (100 per JSON-RPC listener, 1024 by default on Esplora and streamws), for as long as it kept the connection open. A write that makes no progress now closes the connection after -rpcservertimeout on JSON-RPC, as libevent's write timeout does in Core, and after 60 s on Esplora and streamws and on JSON-RPC with -rpcservertimeout=0. The clock runs only while a write is stuck and restarts with every byte the client takes, so a large response to a slow reader, or a stream waiting for its next event, is not cut off.
  • Shutdown closes WebSockets and SSE streams (#822). At shutdown the listeners stopped accepting and closed idle connections, but an upgraded streamws /ws socket had no shutdown signal, and hyper's graceful shutdown waited for an SSE response, on streamws /sse and on Esplora, that never finished. Both stayed open until the process exited, and their clients got no close. A /ws socket now gets a 1001 Going Away close frame and an SSE stream ends its response.
  • Timeouts too large for the clock are clamped (#826). An -rpcservertimeout or -esplorarequesttimeout above about 9.2e18 seconds made every connection task panic when hyper computed its deadline, so the process stayed up and the listener answered nothing. Timeouts above a year are now treated as a year.
  • -esplorarequesttimeout=0 leaves TLS working (#826). The value doubled as the Esplora TLS handshake budget, so 0, documented as leaving idle connections open, also gave every TLS handshake no time and TLS on Esplora could not complete. With 0 the handshake now gets 30 s, the option's default.
  • The plain metrics listener has a connection cap and a header timeout (#826). It ran hyper without a timer and accepted every socket, so a socket that sent nothing was held forever, and any number of them were. It now serves at most 64 connections, as its TLS sibling does, and closes one that sends no request head within 30 s. On both metrics listeners, and on MCP, a write that makes no progress for 60 s closes the connection, as on the other HTTP surfaces. It still binds loopback by default.

Operator

  • Floodable warnings are budgeted per listener (#825). Most accept loops already logged the first five refusals a minute and then a count. Three lines were left out:

    • "TLS handshake timed out" was logged once per socket on the JSON-RPC TLS and Esplora TLS listeners (and on Electrum, MCP and events gRPC TLS), so a client opening sockets and sending nothing could drive it at the cap divided by the handshake budget, about 34 lines a second on Esplora with its defaults.
    • "accept error" was logged on every failed accept, which during fd exhaustion is about 20 lines a second per listener. The Electrum, Stratum and plain metrics loops did not back off at all, and now wait 50 ms as the others do.
    • Stratum's at-capacity budget was shared by its V1, V1-TLS and V2 listeners, so a flood on one silenced the others' reports. Each listener now has its own, and the line names the listener.

    Each of these now has its own budget per listener.

  • reindex and reindexchainstate in bitcoin.conf are honoured (#840). Both keys were accepted in the config file and then ignored, so an operator who asked for a rebuild there got a normal start. They are now read from the file, as Core reads them, in Core's hyphenated spelling too. As in Core, a file value rebuilds on every start until it is removed.

  • conf.d/local.conf overrides what the stack writes (#843). satd-init appended local.conf to the rendered bitcoin.conf so that its lines would win. satd, like Core, takes the first value of a repeated key, so a local.conf line for a key satd-init already writes, such as port, txindex or prune, was ignored. local.conf's top-level lines now go first, and any [section] blocks in it go last, where a value in the active chain's section wins anyway.

  • The core runtime has at least two worker threads (#1144). It had one worker per CPU, so a 1-CPU host had a single worker. One blocking call during IBD then stopped JSON-RPC and P2P together for minutes at a time, and RPC accepted connections without answering. A 1-CPU host now gets two workers. Larger hosts are unchanged.

  • The shadow verification queue is bounded by memory (#1145). Under the default -consensus=rust-shadow, every transaction the primary engine accepts is queued for the shadow engine with its serialized bytes and a copy of each output it spends. The queue was bounded only by -shadowqueuesize, 4,194,304 entries, which at a few hundred bytes to several KB an entry is gigabytes. On a host with few cores the shadow engine falls behind during IBD, and the queue grew until the process reached its memory limit, without ever reaching the entry cap or logging a drop. The new -shadowqueuemb (default 128, in MiB) bounds the bytes the queued work holds. Work past either bound is dropped, as before, and the aggregated WARN now gives the queue's depth and bytes. The primary engine still verifies every script. -shadowqueuemb=0 restores the entry-count bound alone.

  • AssumeUTXO background validation retries a failed connect (#557). When a historical block failed to connect behind a loaded snapshot, the background connector logged it and stopped, for the life of the process, so one transient RocksDB or block-file write error ended background validation until a restart. It now retries as the IBD connector does: an attempt a second, a getwarnings notice assumeutxo.connect_retry every tenth attempt, and after 30 the assumeutxo_catchup_stalled alert, while it keeps retrying. Both clear when the block connects. A block that fails validation is not retried: the connector stops and raises the same alert with reason=invalid_block, and the snapshot stays unvalidated rather than rejected. A block that cannot connect to the background chain at all (it does not extend the background tip, its parent is unknown, or its stored bytes do not decode) stops it the same way, with reason=cannot_connect.

  • AssumeUTXO conditions are alerts on every surface (#557). A stalled catch-up, a failing handoff and a snapshot proven invalid reached getwarnings only, so the status stream, the alertfile webhooks and satd_alert_active never saw them. They are now the standing alert kinds assumeutxo_catchup_stalled, assumeutxo_handoff_failing and assumeutxo_validation_failed, raised and cleared once per episode like the others. The Operator Manual lists them under Node-health alerts, with their details.

  • AssumeUTXO gauges (#557). /metrics exports satd_assumeutxo_background_attached and satd_assumeutxo_snapshot_rejected on every node, and satd_assumeutxo_background_tip_height and satd_assumeutxo_snapshot_height while a snapshot is loaded.

Storage

  • Block reads no longer wait behind each stored block's fsync (#547). Storing a block held the block-file lock across its fsync, and every block read takes that lock: getblock, Esplora, Electrum and the index backfills. On slow or network storage a 10-50 ms fsync per block queued them all behind it during sync. The fsync now runs after the lock is released, on its own handle to the file, and the block is still on disk before the node records where it is.
  • A block file never grows past its size cap (#547). When a block file filled up, the next record moved to the following file. If that file already existed with too little room left, as can happen after pruning, the record was appended past the 128 MiB cap anyway. It now moves on until the record fits.
  • A re-fetched block frees its download slot during initial sync (#547). A block that getblockfrompeer asked for, arriving while the node was in initial block download, did not release the IBD download slot its peer held for that height, so the peer had one less slot until the download timed out.

Consensus

  • Signet blocks must carry the scheduled difficulty (#838). On signet, satd accepted any nBits a block claimed, as long as the block's hash met it and its signature checked out. Bitcoin Core checks signet nBits against the retarget schedule like on every other chain, and refuses a mismatch with bad-diffbits. satd now does the same, against signet's proof-of-work limit and with no minimum-difficulty exception. The default signet's whole header chain passes the check.
  • -reindex checks every block's header as header sync does (#506). A full -reindex rebuilds the block index from the block files, and it checked each block's proof of work but none of the header rules accept_header applies when a header arrives from a peer. A block whose nBits broke the difficulty schedule, whose timestamp was not above its branch's median time past, whose version was below the minimum for its height, or that contradicted a checkpoint could therefore be indexed. Block connection checks neither the difficulty nor the checkpoints, so a block that broke those was also connected: either at once, when its branch claimed the most work, or later, when a reorg moved onto a side branch the reindex had indexed. The reindex now applies those rules, along with the checkpoint rule against forks below the last checkpoint and the network's proof-of-work limit, to every block before it picks the chain to replay. A block that breaks one is logged with the rule, and neither it nor any block built on it is replayed or indexed, as header sync never indexes them. A side-chain block whose parent was not indexed is no longer indexed either. The rule against timestamps more than two hours ahead is not applied to blocks from the files, as the node does not apply it to stored blocks elsewhere. Block files a node wrote while following the network hold no such block, so a reindex over them gives the same result as before.

Release tooling

  • sign-tarballs.sh keeps its downloads across a failed run (#903). The maintainer's signing script downloaded every tarball and SBOM into a temporary directory and deleted it on exit, failed runs included, so a mistyped passphrase meant downloading all of them again, 431 MB for 0.6.0. It now works in ${SATD_SIGN_DIR:-${XDG_CACHE_HOME:-~/.cache}/satd-sign/<tag>}, created mode 0700, and refuses a directory it does not own, a symlink, or one others can write to. A rerun fetches SHA256SUMS and the .sha256 files fresh, keeps a downloaded file only if its SHA-256 matches both SHA256SUMS and the digest GitHub reports for the asset, and keeps each .minisig that still verifies. The directory is removed after a successful upload or --dry-run. --images is unchanged.

SDKs

  • A resilient watch re-anchors after a drop before its first confirmed event (#777). The node puts a resume cursor on a connected block and on a confirmed match only, so a resilient watch or subscription started without FromCursor (Go) or from_cursor (Rust) and with an empty cursor store had nothing to resume from until one arrived. A reconnect in that window sent no SetCursor, got no CursorAccepted, and did not replay the outage, and an event in flight at the drop, such as the block confirming a payment whose mempool match had just arrived, was never delivered. Subscribe and Watch responses now carry a satd-head-cursor header, the position the stream starts from, built like a connected block's cursor at the tip and read after the stream has subscribed to the live bus. Both SDKs adopt it when they have no anchor of their own, so the next reconnect replays from it and the cursor store holds it after the next commit. Against an older node, or one with no block source, the SDKs behave as before. The Go end-to-end test that failed about one run in two on this window now has a variant that cuts on the unconfirmed match.

Packaging

  • The Nix build of satd is reproducible across x86_64 CPUs (#321). jemalloc's build script reads the virtual-address width of the CPU it is built on, 48 bits on most x86_64 CPUs and 57 on those with 5-level paging, and sizes its internal radix tree from it. Two nix build .#satd runs of the same commit therefore produced different satd binaries whenever one build host had 5-level paging, which is why the two-replica check in CI failed now and then. The flake now pins the address width, page size and hugepage size on x86_64, so the binary is the same whichever CPU builds it. sat-cli does not link jemalloc and was never affected. The CI check also keeps both binaries and reports a per-section diff when they differ.
  • flake.lock is committed (#321). The flake had no committed lock, so each Nix build resolved nixpkgs, crane, rust-overlay and flake-utils on the day it ran. Two builds on the same day matched, but a rebuild of a release tag weeks later could pick up newer inputs and produce a different satd. Every build of a commit now evaluates the same inputs.
  • The x86_64 Linux release tarballs no longer depend on the build runner's CPU (#321). release.yml builds its tarballs with cargo, not Nix, and jemalloc read the runner's address width and page sizes there too, so two builds of one tag could differ by which runner built them. The gnu and musl x86_64 builds now pin the same values as the flake. Matching a Nix build to a release tarball byte for byte is still out of scope (packaging chapter, "Reproducible build via Nix").

Upgrade notes

A drop-in upgrade from 0.7.0, with these changes to look for:

  • A client that reads warnings from getblockchaininfo, getnetworkinfo or getmininginfo as a string gets [] on a node with no warnings. It already got an array whenever a warning was active, and Core has returned an array since v28. Start the node with -deprecatedrpc=warnings to keep the string form for now.
  • A bitcoin.conf carrying reindex=1 or reindexchainstate=1 now rebuilds on start. Remove the line if the rebuild is not wanted.
  • In the docker-compose stack, a conf.d/local.conf line for a key satd-init writes now takes effect. Check that file for stale lines before upgrading.
  • A node whose shadow queue held more than 128 MiB now drops shadow work past that and logs it. Raise -shadowqueuemb to keep more.
  • The Electrum server reports a new default name, satd-electrs-electrumx-compatible/<version>. A client or monitor that matched the old satd-electrs-compatible name exactly should match the new one, or set electrumservername to keep the old name. -electrummaxbatchrequests now defaults to 345; an operator who wants the old limit can set it to 100, but Sparrow then fails to verify a wallet with more than 100 transactions.
  • A custom signet (-signetchallenge) whose blocks do not follow the standard retarget schedule, such as one run by a modified bitcoind with a shorter block interval, no longer syncs. Bitcoin Core refuses it too.
  • Log consumers that matched Disconnecting peer on request should match Disconnecting peer with reason="rpc_request" instead.
  • The AssumeUTXO warnings have new getwarnings ids: assumeutxo-validation-failed is now alert.assumeutxo_validation_failed, assumeutxo.catchup_halted is now alert.assumeutxo_catchup_stalled, and assumeutxo.handoff_failing is now alert.assumeutxo_handoff_failing. A status consumer sees three new kinds (proto values 8 to 10); the Rust and Go SDKs in this release name them, and an older SDK reports them as unknown kinds with their severity and message intact.
  • A JSON-RPC client that pauses mid-response, reading nothing for longer than -rpcservertimeout while the server has more to send, is now disconnected. Raise -rpcservertimeout if a client legitimately does that.

About this release

satd is free software under the MIT License.

  • Stability & compatibility — Tier 1 surfaces (Bitcoin Core-compatible
    JSON-RPC wire shape, CLI flag names/defaults, bitcoin.conf keys, on-disk
    layout, Electrum/Esplora surfaces) are governed by
    STABILITY_POLICY.md. satd follows
    semantic versioning: each feature
    release raises the minor version, and deprecations may be accelerated (see
    the policy).
  • Security & disclosure — report vulnerabilities per
    SECURITY.md. Consensus-affecting bugs are treated as
    P0 with same-day acknowledgement.
  • Verifying this release — tarballs are signed with minisign, container
    images with cosign (keyless OIDC), and git tags with SSH signatures (no GPG).
    The key matrix and verification steps are in
    SECURITY.md.
  • Intentional differences from Bitcoin Core — catalogued in
    CORE_DIFFERENCES.md.
  • Operating satd — observability, the full flag matrix, tuning, and the
    downstream packaging contract live in the
    Operator Manual.

What's Changed

  • release/0.7.x: open 0.7.1-pre, backport #1163 and #1158 by @bkeroack in #1167
  • canary: give the Stratum V1 block waits room for difficulty-1 luck (#1153) by @bkeroack in #1168
  • 0.7.1: Content-Type leniency, getrawtransaction not-found text, disconnect/onion log lines, two core workers by @bkeroack in #1166
  • mempool: testmempoolaccept forgives what an allow rule forgives (#999) by @bkeroack in #1169
  • 0.7.1: Core parity in getnetworkinfo, warnings, tx version, config reindex and the stack's local.conf by @bkeroack in #1170
  • validation: bound the shadow verification queue by bytes (#1145) by @bkeroack in #1171
  • p2p: download IBD blocks within a height window, not a shuffled count (#1154) by @bkeroack in #1172
  • rpc: recheck a WebSocket's bearer token on every frame (#821) by @bkeroack in #1173
  • p2p: onion fixed seeds for testnet4 and testnet3, refreshed for mainnet and signet (#1162) by @bkeroack in #1174
  • http: write timeouts, shutdown of upgraded streams, warning budgets and layer order (#823, #822, #825, #826) by @bkeroack in #1177
  • storage: fsync outside the flat-file lock, rotate past a full file, release a refetched block's IBD slot, assert check_block's height (#547, #551) by @bkeroack in #1178
  • electrum: a default server name Sparrow batches against, and a batch cap that takes Sparrow's largest batch (#1069) by @bkeroack in #1179
  • tests: races in the webhook and Esplora mempool tests (#794) by @bkeroack in #1180
  • validation: check signet nBits against the retarget schedule, as Core does (#838) by @bkeroack in #1182
  • canary: wait for Core to receive a Stratum-found block instead of failing on the first not-found by @bkeroack in #1184
  • sign-tarballs.sh: reuse downloads across failed runs; bdk-canary: drop rustls-webpki 0.101 by @bkeroack in #1189
  • canary: bump the downstream pins to their latest stable releases by @bkeroack in #1190
  • reindex: hold every block to the header rules, and close the #491 test gaps by @bkeroack in #1191
  • AssumeUTXO: retry a failed background connect, and alert on every surface by @bkeroack in #1192
  • Streaming: report the stream's head cursor, and resume from it before the first confirmed event by @bkeroack in #1193
  • nix: make the satd flake build reproducible across x86_64 CPUs (jemalloc host probes) by @bkeroack in #1194
  • chore(release): cut 0.7.1 by @bkeroack in #1198

Full Changelog: v0.7.0...v0.7.1