Repository navigation
v0.5.2
📄 Canonical release notes:
docs/release-notes/0.5.2.md
satd 0.5.2
satd 0.5.2 is a patch release on the 0.5.x line with two themes. The first is
deployment: a docker-compose reference stack, a bootable appliance image, and
app-store packages for Umbrel and StartOS, all sharing one first-start script
and one certificate scheme. The container image also drops from 670 MB to
136 MB. The second is Bitcoin Core parity: the defects filed from the deep
review of the 0.5.1 functional-test wave (#660–#673) and the ones Core's own
test suite went on to find, closed in Core's direction.
Upgrade action: the binary is a drop-in replacement — no reindex, no flag
removed. But several fixes change behaviour a deployment may be relying on: a
-connect-pinned node stops listening, -prune is measured in MiB,
-whitelist grants narrow, a peer hostname is refused under -proxy, and the
transaction builders reject more input. Read Upgrade notes
before rolling out.
Highlights
- Three ways to run satd beyond a bare binary — a
reference stack, an appliance image, and Umbrel/StartOS packages, each
installed and driven on a real server before being described as working. - The container image is 136 MB, not 670 MB, and
shipssat-tuiand aHEALTHCHECK. sat-cliandsat-tuispeak TLS to
satd's own-rpctlsbindlistener.- A mistyped RPC argument no longer discards the arguments after it;
ongenerateblockthat had mined a block the caller asked not to connect. createrawtransactionandcreatepsbtcheck the address network and
parse amounts exactly —createpsbt
had built ~5% of five-decimal amounts one satoshi short.- Security:
-whitelistno longer reaches Tor or outbound peers,
and a proxied node no longer resolves peer hostnames through the local
resolver. - Mempool policy at Core parity — dust thresholds,
feerate-diagram RBF, ephemeral dust andsubmitpackage's result shape. - Initial block download could wedge for good on a fork-heavy network —
a competing header could permanently replace the next block to fetch. Fixed,
and a damaged datadir heals on the first restart (#738).
Deployment
The Operator Manual chapter
Appliance & Reference Stack
is the full description.
The reference stack
contrib/stack/ is a docker-compose deployment: satd with JSON-RPC, Electrum,
Esplora, metrics and optionally MCP, each TLS-terminated by a certificate the
install issues for itself on first start.
cd contrib/stack && cp .env.example .env && docker compose up -dOverlays add LND in Neutrino mode with Ride The Lightning, Core Lightning, a
Cashu mint, BTCPay Server, and a Caddy proxy that terminates TLS for the web
UIs and metrics. The plain listeners bind the container network only and are
never published to the host. The internal RPC port is pinned to 8332 on every
network, so in-container sat-cli needs -rpcport=8332 off mainnet. Every
deliverable runs a fully indexed node — Electrum and Esplora require
txindex, which excludes prune — and signet is the default everywhere.
The appliance image
contrib/appliance/ builds a bootable VM. The core flavour is a headless
node; desktop adds XFCE with Sparrow, Electrum and Liana pointed at the
node's Electrum server. Each bundled application is installed from its own
release and checked against a pinned signing key; the build fails rather than
install anything that does not verify. satd-appliance set-network mainnet
switches chains, and satd-appliance enable lightning starts an overlay.
Everything unique to an install — console password, CA, server certificate,
MCP token — is created on first boot, and the build asserts none of them are
present in the artifact. CI brings the stack up on regtest, probes every TLS
listener against the generated CA (each paired with the negative control that
the same handshake without the CA fails), has LND sync over Neutrino, then
boots the image under QEMU. Those runs compile satd from scratch, so they run
on release tags, on demand, and on pull requests labelled appliance-ci.
The appliance image and the stack's overlays bundle third-party software on
a best-effort basis, for evaluation and testing. We do not track its
security advisories in real time. satd itself in the image is the same
supported release as the tarballs and the container image.
Umbrel and StartOS packages
contrib/packaging/ holds packages for Umbrel and StartOS. They
bundle nothing third-party — satd, sat-cli, sat-tui and MCP — and run the
same satd-init the reference stack does, from the same image, so the CA,
certificate, bitcoin.conf and MCP token come from one piece of code.
Both were installed on a real server and driven through every interface they
export: x86_64 on both, aarch64 for StartOS. Doing so found three defects
no static check would have: a container health gate on /readyz that reported
the whole initial sync as a fault, a StartOS Network action that moved the
P2P port without restarting the daemon, and a manifest pinning an image that
predates satd-init. The aarch64 Umbrel package is unverified — umbrelOS
ships aarch64 only as a Raspberry Pi image — and StartOS backup/restore is
unexercised.
TLS everywhere, from one script
contrib/stack/tls/mkca.sh creates one CA per install and issues one server
certificate every satd surface presents. Clients import the CA once; later
reissues are signed by a CA they already trust. The certificate names
<hostname>.local as well as the machine's addresses — connect by the name,
which survives a DHCP change.
sat-cli and sat-tui over TLS
Both clients accept -rpctls, -rpccacert, and -rpcclientcert /
-rpcclientkey for a listener started with -rpcmtls:
sat-cli --rpctls --rpccacert=satd-ca.crt --rpcport=8336 getblockchaininfoWithout the flags nothing changes. -rpccacert without -rpctls is an
error, because ignoring it would send the credential over plain HTTP; a CA
file with no PEM certificates is an error, because it would add no trust
anchor and fail later as a generic handshake error. Point -rpccacert at the
certificate that issued the server's — for a self-signed node certificate,
the certificate itself.
The container image
docker exec -it satd sat-tui works, and the image has a HEALTHCHECK so
depends_on: condition: service_healthy means something. The probe reports
liveness: any HTTP status line from the RPC listener, a 401 included. Set
SATD_RPCPORT for a non-mainnet container. SATD_HEALTH_URL points it at an
endpoint where only a 2xx counts — but not /readyz for a container
health gate: that is 503 until the tip is within six blocks of the headers
tip, so the container reports unhealthy for the entire initial sync.
The image is 136 MB, down from 670 MB. [profile.release] has carried
debug = "line-tables-only" since 0.4.0, and every docker pull since has
been about half a gigabyte of DWARF. Tarballs were never affected —
release.yml already stripped them and split out a signed .debug sidecar.
Rebuild with --build-arg STRIP_BINARIES=0 for an unstripped image.
MCP is reachable by hostname (#734)
The MCP transport validates the Host header against a DNS-rebinding
allowlist, and satd left it at the library's loopback-only default. Any client
that reached the listener by name — every reverse-proxied and app-store
deployment — got 403 Forbidden before authentication ran, even with
-mcpallowremote, -mcpauth and TLS all correctly configured.
-mcpallowedhost=<host[:port]> (repeatable, comma-separated) names the
further authorities the listener accepts. It is additive: the loopback names
stay, and there is no value that turns the check off. A non-loopback bind
without it logs a startup warning naming the symptom. satd-init derives it
from the hostname it issues the certificate for, plus SATD_MCP_ALLOWED_HOSTS
for a proxy or alias. StartOS assigns names the package cannot read, so that
package gets an MCP Hostnames action the operator fills in once.
Initial block download could wedge on a fork-heavy network
satd's height→hash index is the connected chain up to the tip and, above it,
is meant to be the best-header chain: the IBD scheduler, the connector and
the getheaders locator all read "the next block to fetch" from it. Header
acceptance wrote that region for every header it accepted, whatever branch
it was on, so a competing header at an already-mapped height replaced the row
and nothing put the best chain's block back. From then on the node asked every
peer for a block none of them held — a request Bitcoin Core answers with
silence, not notfound — and sat at one height indefinitely while its headers
kept advancing. On testnet4, where short forks are routine, a synced node sat
at the same height for a day with eighteen peers.
Rows above the tip are now written only when a batch of headers yields a new
most-work header, and are re-derived along that chain down to the fork point,
so every row above the tip is an ancestor of the best header by construction.
The scheduler is told which heights changed and re-keys them. A startup pass
re-derives the rows above the tip from the best header's ancestry, so a
datadir the old writer damaged heals on the first restart with this release;
it logs a warning with the number of rows it rewrote, and no action is
required (#738).
Bitcoin Core compatibility
Most of what follows was found by running Bitcoin Core's functional test suite
against satd. Breaking marks a change to output or behaviour that a client
may have been relying on; in every case the new behaviour is Core's.
RPC argument handling
- A mistyped argument no longer discards every argument after it (#672).
The parameter reader cleared the rest of the buffer on any type error, so
generateblockwith a badtransactionssilently droppedsubmit=false
and connected a block. Wrong-typed arguments now answer Core's-3
Wrong type passed:naming each one by position, and a later error no
longer buries an earlier mismatch — which in a debug build had closed the
connection with no response. - Breaking: a call with more arguments than the method declares is
-1,
as Core'sIsValidNumArgsanswers, instead of the surplus being ignored
(#688). The message states the bound rather than Core's full help text. - 24 RPCs that read their first argument as a fixed-length array now read it
like the rest of the surface, so the error is Core's rather than a Rust
Debugdump, andsubmitpackage,converttopsbtandutxoupdatepsbt
accept the optional arguments Core declares (#687). Where satd does not
implement an argument's effect it refuses a supplied value by name. - Breaking:
converttopsbtrefuses a transaction carrying signature data
unlesspermitsigdatais true, instead of stripping it silently (#687). - A wrong-length hash and a right-length non-hex string report different
errors, asParseHashVdoes;getblockfrompeer's argument is named
blockhash(#692). sat-cli -namedsends an argument without=as a positional in Core's
reservedargsslot instead of dropping it (#672).generatetoaddressandgeneratetodescriptorhonourmaxtries, which also
puts the first bound on satd's nonce grind (#688).help <command>answers for every registered method, and the listing names
every method satd registers — thirty were missing, including the PSBT
builders and satd's own index, quarantine and address-index RPCs (#692).getnetworkhashpstakes signednblocksandheightso Core's-1
works, and counts the whole window's work: the estimate was low by a factor
of the window size, ongetmininginfo.networkhashpsand the TUI too.getblockstatsaccepts a numeric height and applies itsstatsfilter.
Transaction builders and PSBTs
- Breaking:
createrawtransactionandcreatepsbtdecode the output
address with the node's network and answer-5 Invalid Bitcoin address
for another network's (#689). A mainnet node accepted a testnet address and
paid a scriptPubKey the sender does not control, with nothing in the
transaction left to catch it.validateaddresshad the same hole. - Breaking, and values change: output amounts are parsed as exact
decimals (Core'sParseFixedPoint) instead of throughf64(#689).
createpsbttruncated, so0.29built 28999999 sats; 5,635 of 100,000
five-decimal amounts were one satoshi short."NaN"built a zero-value
output..5,1.,01.0,+1.0,1.000000009,infand surrounding
whitespace are now refused. createpsbtsharescreaterawtransaction's output parser, gaining the
array form ofoutputs(previously ignored — a PSBT with no outputs),
string amounts, and the duplicate and range checks (#689).- The JSON-RPC compatibility layer re-emits request members byte-for-byte
instead of round-tripping throughserde_json::Value, which collapsed
duplicate keys so the duplicate-address check could never fire (#689). A
repeated key is read with its first value, asUniValuereads it. outputsthat is neither object nor array, an explicitnull, and an
emptydataare refused rather than building an empty output set or an
OP_RETURN OP_0(#689).- Breaking:
iswitnessondecoderawtransactionandconverttopsbt
selects the serialization it names. There was no legacy reader (#671). - Breaking: the
replaceable=truecheck was inverted — Core refuses only
when no input signals; satd refused when any input did not (#671). decodepsbt.feeandanalyzepsbt.feeare computed through Core's
GetInputUTXOrules, which check thenon_witness_utxotxid against the
input so a PSBT's author cannot choose the reported fee (#702).
analyzepsbt.estimated_feeratestays absent: Core derives it from a
dummy-signed transaction and the unsigned size would overstate the rate.decodescript.p2shfollows Core'scan_wrap: no address for an
unspendable script, and one for a redeemScript up to 10,000 bytes (#702).validateaddressreports why a string is invalid — Core'serror
string anderror_locationsfor a Bech32 checksum failure, checked against
Core's vectors — and describes destinations in Core's shape:
witness_version/witness_programare omitted for a non-witness address
rather than-1, andisscriptis true for Taproot and anchor outputs.
Test for the field's absence, or readiswitness.
Mempool policy
-
Breaking: a replacement is judged against the feerate diagram it
displaces, asImprovesFeerateDiagramdoes, not against each conflict's
own feerate (#660). A cheap parent with an expensive child is one chunk to
a miner; beating the parent alone no longer replaces it, even when the
replacement pays more in total. The diagram is exact for the one-parent or
one-child topologies Core's pre-cluster rule covers; larger conflicts keep
the per-conflict rule (recorded inCORE_DIFFERENCES.md). -
RBF Rule 5 counts distinct clusters, not conflicts, so a replacement
conflicting with a hundred children of one parent is accepted (#660).
testmempoolacceptandsendrawtransactionrun one shared check over the
transitive ancestor set, so they cannot disagree. -
prioritisetransactionreaches the relay floor on both paths and mempool
eviction;getprioritisedtransactionsreportedin_mempool: falsefor
every resident transaction (#660). -
Breaking: dust thresholds are Core's (#661). satd charged 68 vbytes to
spend a witness output where Core charges 67, 107 for P2SH where Core
charges 148, and truncated where Core rounds up:script was now P2SH 417 540 P2WPKH 297 294 P2WSH / P2TR 333 330 P2A (pay-to-anchor) 333 240 Lightning anchor outputs are built against 330; a node that calls 330 dust
refuses a commitment transaction its peers relay. -
Breaking: one dust output is standard, and a dusty transaction that
pays any fee — base or priority delta — is refused
dust, tx with dust output must be 0-feeon the single-transaction path as
well as insubmitpackage(#661).-dustrelayfeereaches every dust
decision, so-dustrelayfee=0actually switches the policy off. -
Breaking: bare P2PK and witness programs at versions satd has no name
for — pay-to-anchor among them — are standard, asSolverhas them, and
bare multisig is bounded at x-of-3 (#661). -
The ephemeral dust rule holds on the single-transaction path (#703); a
refused child no longer strands its zero-fee dust parent in the mempool
(#673), the refusal ismissing-ephemeral-spendswith a package result of
unspent-dustrather thanbad-txns-inputs-missingorspent(#673); and a
dust parent now emits a mempoolEnterevent (#704). -
Breaking:
submitpackageappliesmaxfeerateandmaxburnamount
instead of refusing them by name, bounds its array to 1..25, checks Core's
child-with-parents topology, answers with an entry for every submitted
wtxid (package-not-validated,other-wtxid), reports
fees.effective-feerate/effective-includes, and always emits
replaced-transactions(#665). -
Breaking:
testmempoolacceptreportswtxidon every result, reports
txn-already-knownonly while one of the transaction's outputs is still an
unspent coin (it consulted the txindex, which finds a confirmed transaction
forever), names a missing inputmissing-inputs, and parsesmaxfeerate
asParseFeeRate— a negative or out-of-range value is an error, not the
default (#665). -
Breaking: reject reasons match Core's:
mempool-script-verify-flag-failed
on the relay path andblock-script-verify-flag-failedinconnect_block;
a failed diagram check isreplacement-failed;min relay fee not met
carries"<fee> < <required>"; an output value with the high bit set is
bad-txns-vout-negative(#671, #660, #665). -
-minrelaytxfee,-dustrelayfeeand-incrementalrelayfeerefuse a
value outside Core's money range instead of overflowing (#660). -
reorgis documented as a mempool eviction reason; the node had emitted
it all along (#670).
Mining
- Breaking:
getblocktemplateproposal mode validates the way
submitblockdoes, by callingconnect_blockand discarding the batch
(#663). Its own loop skipped script verification, BIP 68, sigop cost and
BIP 30, so a miner was told a block would be accepted when it would not.
A known block answersduplicate/duplicate-invalid/
duplicate-inconclusive; an unknownmodeis-8 Invalid mode. - Breaking: a block both oversized and merkle-broken reports
bad-txnmrklroot, asCheckBlockorders it, andcheck_blockapplies
Core's legacy-sigop ceiling before any prevout is resolved (#663). -blockversionoverrides the template's version on regtest; it was
accepted and ignored (#663).- The template refuses a spend of an immature coinbase, which a reorg can
leave in the mempool and which costs the miner the whole block (#670). -blockmintxfeeis applied — it was parsed and read nowhere — on the
package feerate as Core'saddPackageTxsdoes. Its default is Core's
1 sat/kvB rather than 1000, and it takes the BTC/kvB spelling in
bitcoin.conflike the other fee-rate options (#661).
Fields that were placeholders
A set of RPC fields returned constants chosen to look plausible (#667, #702).
Each now comes from the node:
getpeerinfo.permissionswas[]while the peer's permissions were
populated all along; it reports Core'sToStringsof the real flags, and
nobancarriesdownloadas Core's flag does.getnetworkinfo:relayfee/incrementalfee(the fixed value was ten
times satd's default, contradictinggetmempoolinfo), the service flags
actually advertised,localrelay, and the realwarnings;
getmininginfo.warningslikewise.getblockchaininfo:prunedfollows-prunewithprune_target_size/
automatic_pruning;size_on_diskis a maintained total of block-file
bytes instead of0;pruneheightis persisted and reported with Core's
meaning — the lowest height from which every block to the tip is stored.getchainstates[].coins_tip_cache_bytesreports the configured budget;
getrpcinfo.active_commandslists the requests actually executing;
getpeerinfo.inflightcarries the heights outstanding to that peer during
IBD (empty after, recorded inCORE_DIFFERENCES.md);
getpeerinfo.session_idcarries the BIP 324 session ID.savemempoolwrote nothing while returning success; it writes
mempool.datand returns{"filename": …}.getmemoryinfo.lockedis all zeros: satd has no secure allocator, and it
had reported the process RSS as the pool.estimaterawfeeomits a horizon that does not track the target and the
bucket fields satd has no data for, answers Core's "insufficient data"
error, and range-checksthreshold, instead of returning one feerate for
every horizon withdecay: 0.loggingreads and writes the node's live filter instead of a static map
that claimed 30 categories enabled on a node with no-debug. It lists the
twenty categories satd can act on, honours Core's wildcards, and rejects
none/0as Core does — they are-debugconfig spellings, and
accepting them silently turned off all logging. A SIGHUP re-derives
verbosity from the config file.txindex.syncedstays the flag, deliberately: satd indexes inline, and
the strict predicate would hang Core's "pollsynced" pattern forever on
a datadir first synced without-txindex. Recorded in
CORE_DIFFERENCES.md.
Deployments, chain tips and proofs
- Breaking:
getdeploymentinforeports buried deployments under Core's
output names —dersigisbip66,cltvisbip65— and honours the
blockhashargument it used to ignore (#666). It reports Core v31.1's
full set:taprootmoves fromburiedtobip9,testdummyruns a real
BIP 9 state machine, and the result carriesscript_flags(#692).
-vbparamsis accepted fortestdummy;taprootis refused by name. getchaintipsmaintains a leaf set instead of scanning the whole block
index on every call; a stored-but-unvalidated branch isvalid-headers
rather thanvalid-fork, and the order is deterministic (#662).- Breaking:
verifytxoutproofrequires the block to be on the active
chain and the proof to cover the whole block, answering
-5 Block not found in chainotherwise; a proof built on a stale branch
read as valid (#671). That check,getchaintxstatsandgettxoutproof
answer active-chain membership from the height index instead of walking
from the tip. gettxoutproofwithout ablockhashscans every output index a block can
hold instead of the first 100 (#671).dumptxoutsetaccepts Core'stypeargument (every Core-shaped call was a
parse error) and resolves a relativepathagainst the datadir. It also
reportsnchaintx, without which a snapshot could not become an AssumeUTXO
anchor (#692).estimatesmartfeevalidatesconf_target;generatetodescriptortakes
the descriptors Core takes;getrawmempool verboseandgetmempoolentry
honour-amountunit;-noincludeconfworks (#671).
JSON-RPC transport (#664)
- Breaking: a 2.0 request with no
idis a notification: the method
runs, the node answers204 No Content, and a batch produces no entry.
Theida request carried is echoed,nullincluded, and omitted when
the request carried none. - Breaking: the HTTP status of a legacy (non-2.0) request carries the
error class as Core's does: 400 for an invalid request, 404 for an unknown
method, 500 for any other error. A 2.0 request keeps 200. -rpcservertimeoutbounds the request body (408), idle keep-alive
connections and HTTP/2, and reaches the TLS listener, which had no timeout.- A reply too large to normalise is refused rather than parsed at several
times its size; the engine answers-32008 Response is too bigpast the
20 MiB cap on every listener. Core has no reply cap (#723). - A batch element that is not an object no longer strands the rest of the
batch, and a repeatedjsonrpcmember is judged on its last value (#689).
P2P and peer management
addconnection, Core's regtest-only RPC for opening an outbound
connection of a chosen type, with each type behaving as Core's does:
block-relay-onlywithholds transaction and address relay (it had only
been labelled as such),feelercloses atversion,addr-fetchends
when the peer answers — withaddrv2too, which every BIP155 peer sends.
getpeerinfo.connection_typereports the real type. Breaking: the RPC
returns as soon as the capacity grant is taken rather than after the
handshake; it also accepts a hostname, and requires the newtest:net
capability rather thanrpc:write(#692).-connect=0dialled the address zero. satd read it as a peer and
dialled0.0.0.0:8333at every start. Any-connectnow stops the node
dialling gossiped andpeers.dataddresses, as Core's
m_use_addrman_outgoingdoes;-noconnectis honoured rather than
refused; breaking, a literal-connect=0mixed with a peer address is
refused by name;-connect/-addnodewithout a port take the network's default,
including a bare IPv6 literal (#690).getconfigreports
automatic_outbound, and SIGHUP applies aconnectchange.- Breaking:
-connectand-maxconnections≤ 0 soft-set-listen=0,
and-listen=0soft-sets-listenonion=0, asInitParameterInteraction
does (#690). A pinned node no longer accepts inbound peers, over clearnet
or a hidden service, unless-bind,-whitebindor an explicit-listen
says so. - Breaking: a peer hostname from
-addnode,-connector theaddnode
RPC is refused under-proxy— resolving it locally leaked exactly the
peers a proxied node exists to hide — and under-dns=0(#668). Literal
IPs and.oniontargets are unaffected, and resolution is now async. getpeerinfo:networkisnot_publicly_routablefor an unroutable
peer (an IPv4-mapped IPv6 address is judged as the IPv4 it carries);
addr_relay_enabledis Core's latch rather than a function of direction;
servicesnamesis in bit order;last_block/last_transactionmove
only on acceptance, so a peer cannot hold its eviction protection with
blocks the node already had;getconnectioncountcounts what
getpeerinfolists (#668).getblocksandgetheadersignore locator entries on a stale fork, start
at height 1 rather than re-announcing genesis, and honourhashStop
(#668).addnode <peer> removeclears the manual status (#668).- Automatic outbound connections are bounded as Core's
semOutboundbounds
them;addconnectioncould open unlimitedaddr-fetchandfeeler
connections, and the per-type limits are enforced against dials in flight
(#691). - Answering
getaddrno longer re-enters the peer-table read lock, which
deadlocked the manager's event loop when a writer arrived between the two
acquisitions. banlist.jsonis Core's format, an object keyed bybanned_nets(#669).
A list satd could not read — Core's own — silently disabled persistence,
so everysetbanwas accepted, applied, and gone on restart. It is now
recreated as Core does, the legacy array is read and rewritten in Core's
shape, and the write goes through a temporary file and rename, off the
ban-list lock.
Peer permissions and -whitelist
- Security: a peer arriving over the Tor hidden service is no longer
matched against-whitelist(#701). Tor forwards the service to a local
socket, so every inbound onion peer looked like127.0.0.1and inherited a
-whitelist=127.0.0.1entry — making anonymous remote peers un-bannable
and exempt from the inbound caps.-listenonionnow gets its own listener
on127.0.0.1:<port+1>(Core'sonion_binds) unless abind=…=onion
entry names one, and peers accepted there skip whitelist matching. satd
adds that listener even with an explicit-bind, where Core does not;
recorded inCORE_DIFFERENCES.md. - Breaking (security):
-whitelistis inbound-only unless the entry
carries anouttoken, and anoutentry applies only to manual outbound
connections (#701). satd applied every entry in both directions, so
-whitelist=noban@<subnet>made outbound peers in that range un-bannable. - Breaking: a bare
-whitelist=<subnet>no longer grantsaddr, and
-whitelist=@<subnet>— Core's "match, grant nothing" idiom — grants
nothing instead of the implicit set includingnoban(#667). - Breaking: a
-whitelistentry with a direction and no permission is
refused at startup, and-whitebindrefuses anouttoken, as Core does
(#701).
Other fixes
contrib/stack/tests/smoke.shruns on macOS: it resolvesgtimeoutand
shasum -a 256, preflights every tool it needs by name, and refuses
LibreSSL rather than emit TLS passes that verified nothing (#733).- The reference stack no longer overrides the container
HEALTHCHECKwith
SATD_HEALTH_URL=…/readyz, which reported every initial sync as a fault. - CI: every
apt-get updatedrops the runner image's third-party apt
sources first; a hash-sum mismatch on a repository none of the installed
packages come from was failing every canary job.
Upgrade notes
No action is required to upgrade: the data directory format is unchanged and
no flag is removed. The changes below alter behaviour an integration may have
been relying on.
A -connect-pinned node no longer listens
-connect in any spelling (-connect=0, -noconnect included) and
-maxconnections ≤ 0 now soft-set -listen=0, which in turn soft-sets
-listenonion=0. If a pinned node must still accept inbound connections,
add an explicit listen=1 — or a bind / whitebind, either of which
holds the default up. -connect and -addnode entries without a port now
take the network's default P2P port instead of 8333; write :8333 explicitly
if that is what you meant on a non-mainnet node. -noconnect now starts the
node where it used to refuse.
A peer hostname no longer resolves under -proxy or -dns=0
An -addnode / -connect / addnode entry naming a hostname is refused
with a message saying so, instead of being resolved by your local DNS server.
Replace it with a literal IP or a .onion address. Literal IPs and .onion
entries are unaffected in every mode.
-prune is measured in MiB
Core's -prune=<n> is n * 1024 * 1024 bytes; satd read it as
n * 1_000_000. -prune=550 now reports prune_target_size of 576716800
instead of 550000000, and the node keeps correspondingly more block data.
-prune=1 — Core's spelling for manual pruning, which satd does not
implement — is refused at startup instead of being read as a 1 MiB
budget that silently deleted block data. Use -prune=<MiB> or -prune=0.
-whitelist grants are narrower, and scoped by direction
- A bare
-whitelist=<subnet>no longer grantsaddr;-whitelist=@<subnet>
grants nothing. Name what you need explicitly:
-whitelist=noban,relay,mempool,download,addr@<subnet>. - Entries are inbound-only unless they carry
out, and anoutentry
applies only to manual outbound connections. If an entry was meant to cover
peers you dial, add the token:-whitelist=noban,out@<subnet>. - A
-listenonionnode binds one extra loopback socket on<port+1>, and
onion peers are never whitelist-matched.
banlist.json is written in Bitcoin Core's format
The upgrade is transparent: satd reads both forms, so an existing ban list
carries over. Downgrading is not. A pre-0.5.2 satd cannot parse the
object form, and its response to that is the defect this release fixes — it
silently stops persisting bans. Before rolling back, delete banlist.json
from the network data directory and re-apply any bans you need.
createrawtransaction / createpsbt reject more input
Both refuse an address from another network with -5 Invalid Bitcoin address. Amounts are exact decimals: .5, 1., 01.0, +1.0,
1.000000009, NaN, inf and surrounding whitespace are now
-3 Invalid amount. Values change where the two disagree — createpsbt
truncated, so 0.29 built 28999999 satoshis and now builds 29000000. If you
compare PSBTs across the upgrade, expect single-satoshi differences on
roughly 5% of decimal amounts, each one now the decimal you wrote. A
duplicate output key is now actually detected, and a data value must be
non-empty even-length hex.
Other shape changes to check a client against
getdeploymentinfo:dersig→bip66,cltv→bip65;taprootis
type: "bip9";testdummyandscript_flagsare new.validateaddress:witness_version/witness_programare absent for a
non-witness address rather than-1.converttopsbtneedspermitsigdata=trueto strip signature data.- A surplus RPC argument is
-1; a mistyped one is-3. - JSON-RPC 2.0 requests without an
idare not answered; legacy requests
carry the error class in the HTTP status. -blockmintxfeedefaults to 1 sat/kvB, not 1000, and takes Core's BTC/kvB
spelling inbitcoin.conf.estimaterawfeeomitsdecay,scale,pass,failand untracked
horizons;getmemoryinfo.lockedis all zeros;loggingrejectsnoneand
0. All inCORE_DIFFERENCES.md.- Mempool reject strings, dust thresholds, RBF verdicts and
submitpackage/
testmempoolacceptresult shapes are Core's — see
Mempool policy.
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; while in the
0.xpre-1.0 line, 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
- chore(release): open 0.5.2 dev cycle — bump version to 0.5.2-pre by @bkeroack in #683
- rpc: a mistyped argument must not discard the arguments after it (#672) by @bkeroack in #684
- core-functional: re-measure the inventory; getdeploymentinfo, validateaddress, dumptxoutset by @bkeroack in #685
- core-functional: addconnection, real connection types, and -connect=0 by @bkeroack in #686
- rpc: finish the Args conversion, and adopt parse_hash_v (#687, #692) by @bkeroack in #693
- rpc: a surplus argument is an error, maxtries is honoured, and help answers for every method (#688, #692) by @bkeroack in #694
- tests: the Core-functional harness, the wave's test debt, and the ephemeral-dust hole it hid (#695, #673, #703, #704) by @bkeroack in #700
- rpc: stop reporting values that are not derived from the node's state (#667) by @bkeroack in #699
- net:
-whitelistmust not reach peers Core excludes (#701) by @bkeroack in #705 - rpc: derive the fields that were constants chosen to look plausible (#702) by @bkeroack in #706
- net: a ban list satd could not read turned
setbaninto a no-op (#669) by @bkeroack in #698 - net:
-connectCore-parity leftovers — network port,-listen,-noconnect(#690) by @bkeroack in #697 - rpc: the transaction builders decode an address with the network (#689) by @bkeroack in #696
- tests: the harness waits for a requested TLS listener before handing back the node by @bkeroack in #722
- mempool: dust thresholds, the ephemeral rules, and the template floors Core applies (#661, #670) by @bkeroack in #709
- mining: getblocktemplate proposal mode validates like submitblock (#663) by @bkeroack in #710
- rpc: getchaintips reads a leaf set instead of scanning the index (#662) by @bkeroack in #711
- mempool: RBF bounds, cluster counting, and Core's reject strings (#660, #671) by @bkeroack in #712
- rpc: submitpackage and testmempoolaccept take Core's shape (#665) by @bkeroack in #713
- rpc: the #671 remainder — iswitness, proofs, replaceable, and four more by @bkeroack in #716
- rpc: JSON-RPC notifications, ids, statuses and the server timeout (#664) by @bkeroack in #717
- net: peer resolution, getpeerinfo, locators and the outbound cap (#668, #691) by @bkeroack in #718
- rpc: getdeploymentinfo's real deployment set, nchaintx, and addconnection (#692) by @bkeroack in #719
- rpc: the five reported placeholders become measurements (#702) by @bkeroack in #720
- mempool: replacements are judged against the feerate diagram (#660) by @bkeroack in #721
- appliance: a reference stack, a bootable image, and app-store packages by @bkeroack in #708
- appliance follow-ups: run the StartOS tests in CI, scope the lockfile advisories, and correct the health-probe docs by @bkeroack in #726
- rpc: three Core-parity leftovers from the 0.5.2 review by @bkeroack in #727
- smoke.sh: run on macOS, and preflight every tool by name by @bkeroack in #733
- mcp: accept the hostnames clients actually use by @bkeroack in #734
- ci(actions): bump taiki-e/install-action from 2.87.1 to 2.87.8 by @dependabot[bot] in #730
- ci(actions): bump actions/checkout from 4 to 7 by @dependabot[bot] in #728
- ci(actions): bump actions/upload-artifact from 4 to 7 by @dependabot[bot] in #729
- ci(actions): bump actions/setup-node from 4 to 7 by @dependabot[bot] in #731
- ci: the appliance workflow gates on its own file by @bkeroack in #735
- docs(startos): the advisory section rested on a claim that is not true by @bkeroack in #736
- chain: the height-index rows above the tip follow the best-header chain by @bkeroack in #738
- chore(release): cut 0.5.2 by @bkeroack in #737
Full Changelog: v0.5.1...v0.5.2