Repository navigation
📄 Canonical release notes:
docs/release-notes/0.7.1.md
satd 0.7.1
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
-reindexhold blocks to Core's header rules. A signet
block must carry the scheduled difficulty, and-reindexchecks 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-Typeis accepted, so Core's
owncurlexamples work;warningsis always an array; and
getnetworkinforeports-onlynetreachability 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 thestatusstream, webhooks and metrics as
well asgetwarnings(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
satdis byte-identical whichever
x86_64 CPU builds it, andflake.lockis committed (details).
RPC
-
Any request
Content-Typeis accepted (#824). Bitcoin Core reads the JSON-RPC body whatever the request'sContent-Typesays. satd addedapplication/jsononly when the header was missing, so a request that named another type got415 Unsupported Media Type. That included Core's documented form,-H 'content-type: text/plain;', and the form type plaincurl --datasends. Every request is now read as JSON. -
getrawtransaction's not-found message matches the node's index (#1066). With-txindexon, 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.
-
testmempoolacceptagrees withsendrawtransactionunder anallowpolicy rule (#999). With a-policyfilethat holds anallowrule, 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 isAllow.testmempoolacceptreturned the standardness refusal before it ran the policy, so it refused transactionssendrawtransactionaccepted. It now records the same failures, runs the policy dry run, and forgives them on anAllowverdict. APassorQuarantineverdict still refuses with the standardness reason, and a node without anallowrule is unchanged. -
getnetworkinforeports which networks are reachable (#1164).ipv4andipv6were alwaysreachable: true, limited: false, so a node under-onlynet=oniontold 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'sg_reachable_netsdoes:-onlynetdecides, andonionalso needs a proxy that reaches Tor and no-onion=0.networksnow lists Core's five networks:i2pis never reachable, because satd has no I2P transport, andcjdnsis reachable only with-cjdnsreachable. -
warningsis always an array (#956).getblockchaininfo,getnetworkinfoandgetmininginforeturned"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=warningsbrings back Core's deprecated form: one string, the latest warning, or"".-deprecatedrpcwith 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 withoutrpc:submitthat calledsendrawtransactionwith an unknown named parameter got-8 Unknown named parameterinstead 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-rpcwhitelistcheck 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
-authfileand sendingSIGHUP, or letting itsexpirespass, did not stop a socket that was already open from calling what the token had been granted,sendrawtransactionincluded. 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 asubscribemempoolsubscription 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 with401.
Electrum
- Sparrow batches its requests against satd (#1069). Sparrow decides from the
server.versionname whether a server can take batched requests, and batches only for names it knows, such as one containingelectrumx. 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 nowsatd-electrs-electrumx-compatible/<version>, which Sparrow recognises. Cake Wallet still finds theelectrstoken it looks for before probing silent-payment tweaks, and the name still does not start withelectrs, which would turn BlueWallet's batching off. -electrummaxbatchrequestsdefaults to 345 (#1069). Once Sparrow batches, it sends up to 250blockchain.transaction.get_merklecalls 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
IsStandardTxrefuses a version below 1 or above 3 with reasonversion. satd had no version check, sosendrawtransaction,testmempoolacceptand P2P relay accepted a version 0 or version 4 transaction that Core refuses, and satd then relayed it to peers that would drop it.-acceptnonstdtxnlifts 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 calleddisconnectnode. The line now readsDisconnecting peer. It hasreason="rpc_request"only fordisconnectnode, andreason="internal"otherwise, next to the line that already gives the cause. -
Onion peers are logged by their onion address (#1165). The
Peer connectedandPeer disconnectedlines, and the IBD silent-peer warning, printed0.0.0.0:<port>for a.onionpeer. They now print the addressgetpeerinforeports,<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
-maxaheadblocks 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-maxaheadabove the tip, as Core's download window works. Two smaller paths are closed as well:- A block announced by
invduring IBD is answered withgetheadersonly, 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
AcceptBlockrefuses one too far ahead.
Blocks stored above the window by 0.7.0 stay until the tip reaches them.
- A block announced by
-
Onion seeds for every network (#1162). satd had no
.onionfixed seeds for testnet4 or testnet3, so a Tor-only node (-onlynet=onionwith-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 andNODE_NETWORKwhen 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
-rpcservertimeouton 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
/wssocket had no shutdown signal, and hyper's graceful shutdown waited for an SSE response, on streamws/sseand on Esplora, that never finished. Both stayed open until the process exited, and their clients got no close. A/wssocket now gets a1001 Going Awayclose frame and an SSE stream ends its response. - Timeouts too large for the clock are clamped (#826). An
-rpcservertimeoutor-esplorarequesttimeoutabove 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=0leaves TLS working (#826). The value doubled as the Esplora TLS handshake budget, so0, documented as leaving idle connections open, also gave every TLS handshake no time and TLS on Esplora could not complete. With0the 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.
-
reindexandreindexchainstateinbitcoin.confare 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.confoverrides what the stack writes (#843).satd-initappendedlocal.confto the renderedbitcoin.confso that its lines would win. satd, like Core, takes the first value of a repeated key, so alocal.confline for a keysatd-initalready writes, such asport,txindexorprune, 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=0restores 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
getwarningsnoticeassumeutxo.connect_retryevery tenth attempt, and after 30 theassumeutxo_catchup_stalledalert, 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 withreason=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, withreason=cannot_connect. -
AssumeUTXO conditions are alerts on every surface (#557). A stalled catch-up, a failing handoff and a snapshot proven invalid reached
getwarningsonly, so thestatusstream, thealertfilewebhooks andsatd_alert_activenever saw them. They are now the standing alert kindsassumeutxo_catchup_stalled,assumeutxo_handoff_failingandassumeutxo_validation_failed, raised and cleared once per episode like the others. The Operator Manual lists them under Node-health alerts, with theirdetails. -
AssumeUTXO gauges (#557).
/metricsexportssatd_assumeutxo_background_attachedandsatd_assumeutxo_snapshot_rejectedon every node, andsatd_assumeutxo_background_tip_heightandsatd_assumeutxo_snapshot_heightwhile 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
getblockfrompeerasked 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
nBitsa block claimed, as long as the block's hash met it and its signature checked out. Bitcoin Core checks signetnBitsagainst the retarget schedule like on every other chain, and refuses a mismatch withbad-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. -reindexchecks every block's header as header sync does (#506). A full-reindexrebuilds the block index from the block files, and it checked each block's proof of work but none of the header rulesaccept_headerapplies when a header arrives from a peer. A block whosenBitsbroke 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.shkeeps 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 fetchesSHA256SUMSand the.sha256files fresh, keeps a downloaded file only if its SHA-256 matches bothSHA256SUMSand the digest GitHub reports for the asset, and keeps each.minisigthat still verifies. The directory is removed after a successful upload or--dry-run.--imagesis 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) orfrom_cursor(Rust) and with an empty cursor store had nothing to resume from until one arrived. A reconnect in that window sent noSetCursor, got noCursorAccepted, 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.SubscribeandWatchresponses now carry asatd-head-cursorheader, 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
satdis 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. Twonix build .#satdruns of the same commit therefore produced differentsatdbinaries 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-clidoes not link jemalloc and was never affected. The CI check also keeps both binaries and reports a per-section diff when they differ. flake.lockis 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 differentsatd. 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.ymlbuilds 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
warningsfromgetblockchaininfo,getnetworkinfoorgetmininginfoas 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=warningsto keep the string form for now. - A
bitcoin.confcarryingreindex=1orreindexchainstate=1now rebuilds on start. Remove the line if the rebuild is not wanted. - In the docker-compose stack, a
conf.d/local.confline for a keysatd-initwrites 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
-shadowqueuembto keep more. - The Electrum server reports a new default name,
satd-electrs-electrumx-compatible/<version>. A client or monitor that matched the oldsatd-electrs-compatiblename exactly should match the new one, or setelectrumservernameto keep the old name.-electrummaxbatchrequestsnow 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 requestshould matchDisconnecting peerwithreason="rpc_request"instead. - The AssumeUTXO warnings have new
getwarningsids:assumeutxo-validation-failedis nowalert.assumeutxo_validation_failed,assumeutxo.catchup_haltedis nowalert.assumeutxo_catchup_stalled, andassumeutxo.handoff_failingis nowalert.assumeutxo_handoff_failing. Astatusconsumer 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
-rpcservertimeoutwhile the server has more to send, is now disconnected. Raise-rpcservertimeoutif 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.confkeys, 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