Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CORE_DIFFERENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,7 @@ default behavior set the corresponding flag.
| Invalid header in a `cmpctblock` | penalised for most classes of header invalidity (`MaybePunishNodeForBlock` with `via_compact_block`) | ignored, never penalised | A compact block is pushed, not requested, and a header that fails validation leaves no state behind; the block is simply not reconstructed. A `headers` message is still penalised as in Core. |
| `cmpctblock` prefill (`-cmpctblockprefill`) | not available (proposed in #35558) | off; opt in | An extension Core does not ship yet. With it on, a block is announced with the transactions this node lacked when it arrived prefilled, up to `-cmpctblockprefillbytes` (default 8192); receivers need nothing new, since BIP 152 already requires accepting any well-formed prefill. Off, satd prefills the coinbase only, as Core does. |
| `sendheaders` to an outbound peer | once the peer has shown a block past the minimum chain work (`MaybeSendSendHeaders`) | in the handshake | satd answers a block `inv` with a `getdata` for the block before it has the header, where Core sends `getheaders`, so a peer that mines while satd syncs from it would hand it blocks out of order until header announcements start. Inbound peers are sent it on Core's condition, and every peer at most once. |
| A block whose parent is unknown | refused as `prev-blk-not-found`, nothing kept, and the sender discouraged (`MaybePunishNodeForBlock`) | kept until its parent connects if this node asked for it, otherwise dropped; the sender is asked for headers and not penalised | Because a block `inv` is answered with a `getdata` (row above), a block the node asked for can arrive before the parent it is still learning about, and keeping it saves fetching it again. A kept block must carry valid proof of work within the network's limit and be at most 4,000,000 bytes; at most 4 are kept per peer and 64 (32 MB) in all, the oldest going first, each for at most 5 minutes. |
| Compact block reconstructions per block | at most three, unless we requested the block from the sender | at most three, always | satd's tip-following fetch asks every peer for a missing block, so Core's "requested from this peer" exemption would cover nearly every peer. The full block that request asked for still arrives; a sender we asked for a `cmpctblock` is asked for the full block instead. |
| WebSocket upgrade by an `-rpcwhitelist`-restricted user | not applicable (Core serves no WebSocket) | refused with HTTP 403 | satd's JSON-RPC listeners accept WebSocket, and calls over an open WebSocket do not pass the layer that checks a user's method list, so a restricted user may not open one. Unrestricted users, and bearer tokens (scoped by their capabilities, not by `-rpcwhitelist`), are unaffected. `-rpcdisableauth` cannot be combined with an allowlist: that surface authenticates no user to restrict. |
| `--listenonion` | on (no-op without Tor) | off (on if `-torcontrol` set) | Core defaults it on, but it's a silent no-op unless a Tor control port is reachable; satd defaults it off to avoid dialing the control port on every boot. When on, satd creates a v3 hidden service via `-torcontrol` (default `127.0.0.1:9051`). An explicit `-torcontrol` implies `-listenonion=1`; `-listenonion=0` forces it off. |
Expand Down
22 changes: 21 additions & 1 deletion docs/manual/src/ibd.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,23 @@ On an AssumeUTXO node the history below the snapshot base is legitimately
unvalidated until the background chainstate reaches it. That is recognised and
logged at `INFO`, not treated as damage.

### A coin the database cannot read

If RocksDB cannot read a coin from the chainstate (an I/O error or a checksum
mismatch), or a coin's row does not decode, satd stops at once, as Bitcoin Core
does. This holds wherever a coin is looked up: block connection, the mempool,
`gettxout`, the Electrum and Esplora lookups, and the offline
`satd-chainstate-audit` below. The log and stderr say `Error reading from
database, shutting down`, with the outpoint and the error. satd does not carry
on as if the coin were spent: that would refuse a valid block that spends it,
and record the block as invalid across restarts.

The stop is an `abort()` (the process ends with `SIGABRT`), so no shutdown
flush runs. The next start replays the blocks after the last flush from the
block files, as after any crash. If the error comes back after a restart, the
disk or the datadir is damaged: check the disk, then rebuild the UTXO set with
`-reindex-chainstate`, or audit a copy of the datadir first (below).

### Auditing a suspect datadir offline

`satd-chainstate-audit` answers the question the startup checks cannot afford
Expand Down Expand Up @@ -411,7 +428,10 @@ Note also that it is not included in the release tarballs or the Docker image;
build it from source (`cargo build --release --bin satd-chainstate-audit`).

Exit status is `0` when consistent, `1` when it could not run, `2` when it found
inconsistencies, so it scripts cleanly.
inconsistencies, so it scripts cleanly. A coin the database cannot read stops
the tool as it stops the node (see above): it prints `Error reading from
database` with the outpoint and the RocksDB error, which names the damaged
file, and ends with `SIGABRT` before any report.

It diagnoses and does not repair. A missing coin is recoverable only by
replaying the block that created it: `-reindex-chainstate`, or
Expand Down
69 changes: 57 additions & 12 deletions node/src/chain/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7264,20 +7264,28 @@ impl ChainState {

let new_height = parent.height + 1;

// Structural + witness block validation. If the block already has a
// `HeaderOnly` index entry (submitted via `submitheader`) and fails
// body validation, mark it — and every descendant in the index — as
// `Invalid` so that `getchaintips` reports the correct status.
// This matches Core's `InvalidBlockFound`.
// Structural + witness block validation: Core's `CheckBlock` plus the
// witness and weight rules of `ContextualCheckBlock`.
//
// Except for mutation-class rejections, which Core deliberately
// excludes from that marking (CVE-2012-2459): the block hash does not
// commit to the data we just rejected, so the verdict may belong to a
// malleated copy of a block that is actually valid. Writing it down
// would bar the honest block — and, via the parent-status guard, its
// whole descendant chain — until an operator ran `reconsiderblock`.
// A failure here is not written down against the hash, with one
// exception. Core's `ProcessNewBlock` runs `CheckBlock` before
// `AcceptBlock` and returns on failure without marking anything
// (`validation.cpp`: "we will never mark a block as invalid if
// CheckBlock() fails"), so `bad-txnmrklroot`, `bad-txns-duplicate`,
// `bad-blk-length`, `bad-cb-missing`, `bad-cb-multiple` and
// `bad-blk-sigops` leave the index as it was. The witness rules are
// `BLOCK_MUTATED`, which `InvalidBlockFound` skips. That leaves
// `bad-blk-weight`, the one rule here that Core's `AcceptBlock`
// marks `BLOCK_FAILED_VALID`. `check_block` reaches it only after the
// merkle and witness checks have passed, so by then the hash commits
// to every byte that was weighed.
//
// Core marks it on the index entry `AcceptBlockHeader` made. Here that
// is the `HeaderOnly` row a header announcement left; a block with no
// row yet is refused unmarked, at the cost of re-running this check if
// it is sent again.
if let Err(e) = validation::block::check_block(block, self.network, new_height) {
if !e.is_mutation_class()
if matches!(e, validation::ValidationError::OverweightBlock)
&& self
.store
.get_block_index(&block_hash)
Expand Down Expand Up @@ -7361,6 +7369,39 @@ impl ChainState {
let current_tip = self.tip_hash();
let new_chainwork = add_u256(&parent.chainwork, &work_for_bits(block.header.bits));

// A block that extends the tip and arrived without its header first
// (`submitblock`, a pushed `block` message) has no index entry yet.
// Core's `AcceptBlock` creates one in `AcceptBlockHeader` before the
// block can reach `ConnectTip`, so a block that then fails to connect
// is marked `BLOCK_FAILED_VALID` and a copy sent again stops at
// `duplicate-invalid`. Without an entry the mark after a failed
// connect below had nothing to land on: the block was forgotten, and
// every copy sent again was written to disk again and validated again
// in full.
//
// The entry is the `HeaderOnly` row a header announcement would have
// left. A connect writes the `Valid` entry over it and a verdict marks
// it `Invalid`, as on the headers-first path. Any other failure is
// damage to this node's own storage and leaves a known header whose
// block is not stored. Unlike an announced header, that one has not
// moved the best-header pointer, which moves here only after a
// successful connect: a restart seeds the pointer from the index, and
// a descendant header or another copy of the block reaches it.
if prev_hash == current_tip && self.store.get_block_index(&block_hash).is_none() {
let entry = BlockIndexEntry {
header: block.header,
height: new_height,
status: BlockStatus::HeaderOnly,
num_tx: 0,
file_number: 0,
data_pos: 0,
chainwork: new_chainwork,
};
let mut batch = crate::storage::StoreBatch::default();
batch.block_index_puts.push((block_hash, entry));
self.write_chain_batch(batch)?;
}

if prev_hash != current_tip {
// Side chain block — store it first
let entry = BlockIndexEntry {
Expand Down Expand Up @@ -9894,6 +9935,10 @@ impl ChainState {
}
}

#[cfg(test)]
#[path = "state_blockaccept_tests.rs"]
mod blockaccept_tests;

/// Read-side surface for the silent-payment tweak index (the
/// `getsilentpaymentblockdata` RPC, the streaming `tweaks` category replay,
/// and the D4 rescan fast path). Backed by the same durable rows the connect
Expand Down
Loading
Loading