Skip to content

v0.5.2

Choose a tag to compare

@github-actions github-actions released this 12 Sep 14:41
· 158 commits to master since this release
v0.5.2
17a1f93

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

satd 0.5.2

Released 2026-09-11 · git tag v0.5.2 · Changelog

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

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 -d

Overlays 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 getblockchaininfo

Without 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
    generateblock with a bad transactions silently dropped submit=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's IsValidNumArgs answers, 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
    Debug dump, and submitpackage, converttopsbt and utxoupdatepsbt
    accept the optional arguments Core declares (#687). Where satd does not
    implement an argument's effect it refuses a supplied value by name.
  • Breaking: converttopsbt refuses a transaction carrying signature data
    unless permitsigdata is true, instead of stripping it silently (#687).
  • A wrong-length hash and a right-length non-hex string report different
    errors, as ParseHashV does; getblockfrompeer's argument is named
    blockhash (#692).
  • sat-cli -named sends an argument without = as a positional in Core's
    reserved args slot instead of dropping it (#672).
  • generatetoaddress and generatetodescriptor honour maxtries, 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).
  • getnetworkhashps takes signed nblocks and height so Core's -1
    works, and counts the whole window's work: the estimate was low by a factor
    of the window size, on getmininginfo.networkhashps and the TUI too.
  • getblockstats accepts a numeric height and applies its stats filter.

Transaction builders and PSBTs

  • Breaking: createrawtransaction and createpsbt decode 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. validateaddress had the same hole.
  • Breaking, and values change: output amounts are parsed as exact
    decimals (Core's ParseFixedPoint) instead of through f64 (#689).
    createpsbt truncated, so 0.29 built 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, inf and surrounding
    whitespace are now refused.
  • createpsbt shares createrawtransaction's output parser, gaining the
    array form of outputs (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 through serde_json::Value, which collapsed
    duplicate keys so the duplicate-address check could never fire (#689). A
    repeated key is read with its first value, as UniValue reads it.
  • outputs that is neither object nor array, an explicit null, and an
    empty data are refused rather than building an empty output set or an
    OP_RETURN OP_0 (#689).
  • Breaking: iswitness on decoderawtransaction and converttopsbt
    selects the serialization it names. There was no legacy reader (#671).
  • Breaking: the replaceable=true check was inverted — Core refuses only
    when no input signals; satd refused when any input did not (#671).
  • decodepsbt.fee and analyzepsbt.fee are computed through Core's
    GetInputUTXO rules, which check the non_witness_utxo txid against the
    input so a PSBT's author cannot choose the reported fee (#702).
    analyzepsbt.estimated_feerate stays absent: Core derives it from a
    dummy-signed transaction and the unsigned size would overstate the rate.
  • decodescript.p2sh follows Core's can_wrap: no address for an
    unspendable script, and one for a redeemScript up to 10,000 bytes (#702).
  • validateaddress reports why a string is invalid — Core's error
    string and error_locations for a Bech32 checksum failure, checked against
    Core's vectors — and describes destinations in Core's shape:
    witness_version / witness_program are omitted for a non-witness address
    rather than -1, and isscript is true for Taproot and anchor outputs.
    Test for the field's absence, or read iswitness.

Mempool policy

  • Breaking: a replacement is judged against the feerate diagram it
    displaces, as ImprovesFeerateDiagram does, 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 in CORE_DIFFERENCES.md).

  • RBF Rule 5 counts distinct clusters, not conflicts, so a replacement
    conflicting with a hundred children of one parent is accepted (#660).
    testmempoolaccept and sendrawtransaction run one shared check over the
    transitive ancestor set, so they cannot disagree.

  • prioritisetransaction reaches the relay floor on both paths and mempool
    eviction; getprioritisedtransactions reported in_mempool: false for
    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-fee on the single-transaction path as
    well as in submitpackage (#661). -dustrelayfee reaches every dust
    decision, so -dustrelayfee=0 actually switches the policy off.

  • Breaking: bare P2PK and witness programs at versions satd has no name
    for — pay-to-anchor among them — are standard, as Solver has 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 is missing-ephemeral-spends with a package result of
    unspent-dust rather than bad-txns-inputs-missingorspent (#673); and a
    dust parent now emits a mempool Enter event (#704).

  • Breaking: submitpackage applies maxfeerate and maxburnamount
    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: testmempoolaccept reports wtxid on every result, reports
    txn-already-known only 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 input missing-inputs, and parses maxfeerate
    as ParseFeeRate — 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 and block-script-verify-flag-failed in connect_block;
    a failed diagram check is replacement-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, -dustrelayfee and -incrementalrelayfee refuse a
    value outside Core's money range instead of overflowing (#660).

  • reorg is documented as a mempool eviction reason; the node had emitted
    it all along (#670).

Mining

  • Breaking: getblocktemplate proposal mode validates the way
    submitblock does, by calling connect_block and 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 answers duplicate / duplicate-invalid /
    duplicate-inconclusive; an unknown mode is -8 Invalid mode.
  • Breaking: a block both oversized and merkle-broken reports
    bad-txnmrklroot, as CheckBlock orders it, and check_block applies
    Core's legacy-sigop ceiling before any prevout is resolved (#663).
  • -blockversion overrides 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).
  • -blockmintxfee is applied — it was parsed and read nowhere — on the
    package feerate as Core's addPackageTxs does. Its default is Core's
    1 sat/kvB rather than 1000, and it takes the BTC/kvB spelling in
    bitcoin.conf like 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.permissions was [] while the peer's permissions were
    populated all along; it reports Core's ToStrings of the real flags, and
    noban carries download as Core's flag does.
  • getnetworkinfo: relayfee / incrementalfee (the fixed value was ten
    times satd's default, contradicting getmempoolinfo), the service flags
    actually advertised, localrelay, and the real warnings;
    getmininginfo.warnings likewise.
  • getblockchaininfo: pruned follows -prune with prune_target_size /
    automatic_pruning; size_on_disk is a maintained total of block-file
    bytes instead of 0; pruneheight is persisted and reported with Core's
    meaning — the lowest height from which every block to the tip is stored.
  • getchainstates[].coins_tip_cache_bytes reports the configured budget;
    getrpcinfo.active_commands lists the requests actually executing;
    getpeerinfo.inflight carries the heights outstanding to that peer during
    IBD (empty after, recorded in CORE_DIFFERENCES.md);
    getpeerinfo.session_id carries the BIP 324 session ID.
  • savemempool wrote nothing while returning success; it writes
    mempool.dat and returns {"filename": …}.
  • getmemoryinfo.locked is all zeros: satd has no secure allocator, and it
    had reported the process RSS as the pool.
  • estimaterawfee omits 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-checks threshold, instead of returning one feerate for
    every horizon with decay: 0.
  • logging reads 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 / 0 as Core does — they are -debug config spellings, and
    accepting them silently turned off all logging. A SIGHUP re-derives
    verbosity from the config file.
  • txindex.synced stays the flag, deliberately: satd indexes inline, and
    the strict predicate would hang Core's "poll synced" pattern forever on
    a datadir first synced without -txindex. Recorded in
    CORE_DIFFERENCES.md.

Deployments, chain tips and proofs

  • Breaking: getdeploymentinfo reports buried deployments under Core's
    output names — dersig is bip66, cltv is bip65 — and honours the
    blockhash argument it used to ignore (#666). It reports Core v31.1's
    full set: taproot moves from buried to bip9, testdummy runs a real
    BIP 9 state machine, and the result carries script_flags (#692).
    -vbparams is accepted for testdummy; taproot is refused by name.
  • getchaintips maintains a leaf set instead of scanning the whole block
    index on every call; a stored-but-unvalidated branch is valid-headers
    rather than valid-fork, and the order is deterministic (#662).
  • Breaking: verifytxoutproof requires the block to be on the active
    chain and the proof to cover the whole block, answering
    -5 Block not found in chain otherwise; a proof built on a stale branch
    read as valid (#671). That check, getchaintxstats and gettxoutproof
    answer active-chain membership from the height index instead of walking
    from the tip.
  • gettxoutproof without a blockhash scans every output index a block can
    hold instead of the first 100 (#671).
  • dumptxoutset accepts Core's type argument (every Core-shaped call was a
    parse error) and resolves a relative path against the datadir. It also
    reports nchaintx, without which a snapshot could not become an AssumeUTXO
    anchor (#692).
  • estimatesmartfee validates conf_target; generatetodescriptor takes
    the descriptors Core takes; getrawmempool verbose and getmempoolentry
    honour -amountunit; -noincludeconf works (#671).

JSON-RPC transport (#664)

  • Breaking: a 2.0 request with no id is a notification: the method
    runs, the node answers 204 No Content, and a batch produces no entry.
    The id a request carried is echoed, null included, 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.
  • -rpcservertimeout bounds 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 big past 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 repeated jsonrpc member 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-only withholds transaction and address relay (it had only
    been labelled as such), feeler closes at version, addr-fetch ends
    when the peer answers — with addrv2 too, which every BIP155 peer sends.
    getpeerinfo.connection_type reports 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 new test:net
    capability rather than rpc:write (#692).
  • -connect=0 dialled the address zero. satd read it as a peer and
    dialled 0.0.0.0:8333 at every start. Any -connect now stops the node
    dialling gossiped and peers.dat addresses, as Core's
    m_use_addrman_outgoing does; -noconnect is honoured rather than
    refused; breaking, a literal -connect=0 mixed with a peer address is
    refused by name; -connect / -addnode without a port take the network's default,
    including a bare IPv6 literal (#690). getconfig reports
    automatic_outbound, and SIGHUP applies a connect change.
  • Breaking: -connect and -maxconnections ≤ 0 soft-set -listen=0,
    and -listen=0 soft-sets -listenonion=0, as InitParameterInteraction
    does (#690). A pinned node no longer accepts inbound peers, over clearnet
    or a hidden service, unless -bind, -whitebind or an explicit -listen
    says so.
  • Breaking: a peer hostname from -addnode, -connect or the addnode
    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 .onion targets are unaffected, and resolution is now async.
  • getpeerinfo: network is not_publicly_routable for an unroutable
    peer (an IPv4-mapped IPv6 address is judged as the IPv4 it carries);
    addr_relay_enabled is Core's latch rather than a function of direction;
    servicesnames is in bit order; last_block / last_transaction move
    only on acceptance, so a peer cannot hold its eviction protection with
    blocks the node already had; getconnectioncount counts what
    getpeerinfo lists (#668).
  • getblocks and getheaders ignore locator entries on a stale fork, start
    at height 1 rather than re-announcing genesis, and honour hashStop
    (#668). addnode <peer> remove clears the manual status (#668).
  • Automatic outbound connections are bounded as Core's semOutbound bounds
    them; addconnection could open unlimited addr-fetch and feeler
    connections, and the per-type limits are enforced against dials in flight
    (#691).
  • Answering getaddr no longer re-enters the peer-table read lock, which
    deadlocked the manager's event loop when a writer arrived between the two
    acquisitions.
  • banlist.json is Core's format, an object keyed by banned_nets (#669).
    A list satd could not read — Core's own — silently disabled persistence,
    so every setban was 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 like 127.0.0.1 and inherited a
    -whitelist=127.0.0.1 entry — making anonymous remote peers un-bannable
    and exempt from the inbound caps. -listenonion now gets its own listener
    on 127.0.0.1:<port+1> (Core's onion_binds) unless a bind=…=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 in CORE_DIFFERENCES.md.
  • Breaking (security): -whitelist is inbound-only unless the entry
    carries an out token, and an out entry 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 grants addr, and
    -whitelist=@<subnet> — Core's "match, grant nothing" idiom — grants
    nothing instead of the implicit set including noban (#667).
  • Breaking: a -whitelist entry with a direction and no permission is
    refused at startup, and -whitebind refuses an out token, as Core does
    (#701).

Other fixes

  • contrib/stack/tests/smoke.sh runs on macOS: it resolves gtimeout and
    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 HEALTHCHECK with
    SATD_HEALTH_URL=…/readyz, which reported every initial sync as a fault.
  • CI: every apt-get update drops 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 grants addr; -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 an out entry
    applies only to manual outbound connections. If an entry was meant to cover
    peers you dial, add the token: -whitelist=noban,out@<subnet>.
  • A -listenonion node 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; taproot is
    type: "bip9"; testdummy and script_flags are new.
  • validateaddress: witness_version / witness_program are absent for a
    non-witness address rather than -1.
  • converttopsbt needs permitsigdata=true to strip signature data.
  • A surplus RPC argument is -1; a mistyped one is -3.
  • JSON-RPC 2.0 requests without an id are not answered; legacy requests
    carry the error class in the HTTP status.
  • -blockmintxfee defaults to 1 sat/kvB, not 1000, and takes Core's BTC/kvB
    spelling in bitcoin.conf.
  • estimaterawfee omits decay, scale, pass, fail and untracked
    horizons; getmemoryinfo.locked is all zeros; logging rejects none and
    0. All in CORE_DIFFERENCES.md.
  • Mempool reject strings, dust thresholds, RBF verdicts and submitpackage /
    testmempoolaccept result 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.conf keys, on-disk
    layout, Electrum/Esplora surfaces) are governed by
    STABILITY_POLICY.md. satd follows
    semantic versioning; while in the
    0.x pre-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: -whitelist must 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 setban into a no-op (#669) by @bkeroack in #698
  • net: -connect Core-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