Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
29dc4e3
confirm: pin overnight missed confirm operators
reardencode Oct 14, 2026
c56cf94
log: pin overnight missed log and API-log operators
reardencode Oct 14, 2026
1fb9106
mempool: pin overnight missed accept operators
reardencode Oct 14, 2026
0bacc84
mempool: pin overnight missed fee operators
reardencode Oct 14, 2026
8043539
mempool: pin overnight missed graph order
reardencode Oct 14, 2026
d50ed6b
mempool: pin overnight missed orphanage limits
reardencode Oct 14, 2026
20fda8e
mempool: pin overnight missed packed-tx decoding
reardencode Oct 14, 2026
66116f7
mempool: pin overnight missed slot persist
reardencode Oct 14, 2026
3a78148
confirm, mempool: pin overnight mutants on the shipped paths
reardencode Oct 14, 2026
057986e
confirm: park the parent read on the HDD spindle
reardencode Oct 14, 2026
5ff335f
log: truncate API params on a char boundary
reardencode Oct 14, 2026
774cce0
test: take blocksonly listeners from the kernel port
reardencode Oct 14, 2026
56a5733
test: lock the deleted eviction cap and package index compare
reardencode Oct 14, 2026
09aa333
test: pin orphan multi-evict and the free and dead slot counts
reardencode Oct 15, 2026
4cd9555
test: let the package RPC journey own ancestor shape
rearden-grok[bot] Oct 15, 2026
638348b
test: read API log truncation and escapes from the file
rearden-grok[bot] Oct 15, 2026
6a238bd
test: name the mutant pins a socket cannot show
rearden-grok[bot] Oct 15, 2026
91d0bcd
test: read template order and equal-rate eviction from RPC
rearden-grok[bot] Oct 15, 2026
f3596f2
test: grow the witness pad without a dead empty branch
rearden-grok[bot] Oct 15, 2026
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
4 changes: 2 additions & 2 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,11 +342,11 @@ A cell that names a crate-local test describes the witness that exists today. It
| `overlay_config` | Node CLI + net | In-process onion / I2P / cjdns matrix: `onlynet`, proxy, SAM, reachable, accept-incoming, one parse or learn check per network, `--only-net` dial candidates, no cjdns dial until reachable, and one `peers` file round trip for all three. Live Tor, i2pd, and cjdns stay in overlay-functional. |
| `node_cli_and_surface_smoke` | Lifecycle/CLI | Networks, `run_node`, config errors, CLI flags (incl. `--conf`, `--peer-timeout=0` refuse / `=1` smoke, unknown conf key ignored, `min_relay_tx_fee=-1` and `network=nope` conf fail), dropped Core `--rpcuser`/`--rpcpassword`/`--rpcport`/`--rpcconnect`/`-rpcport` refuse, help/version (`rbitcoin-cli` help and version do not dial). Usage errors exit 2 before a datadir is touched: unknown flags, missing or bad values, and every concatenated, one-dash, or Core spelling of a native kebab flag. A missing conf, a bad line, a bad `log_level`, conf `rpcuser`/`rpcpassword`, and `max_outbound=0` exit 2 and open no store. Non-hex `--min-chain-work`, a signet challenge or block time off a custom signet, and `--sp-tweaks` with `--prune-seqsigwit` exit 1. One smoke takes the native flag set together; bare network conf lines parse and CLI `--datadir` wins over conf `datadir`; `--datadir-cold` puts seqsigwit, txstat, and input on the cold store; a custom signet smokes. Signet: genesis header plus height-1 BIP325 connect. The assembled `NodeConfig` values and help text no surface reports are the node-crate `operator_conf_and_argv` (`--bytes-per-sigop`, `--block-reserved-sigops`, conf `sp_tweaks` together with `prune_seqsigwit`, conf `sp_tweaks=1` at the default dust and `sp_tweaks_dust` of 546, 0, or a non-numeric value, and Electrum or Esplora with and without `--sh-index` plus `--sh-index` alone) |
| `three_stage_confirm_and_parent_pin_surface` | Consensus+query | Split load→scripts→write of pad+spend from genesis (header-plan BIP68 MTP); parent pin; load ready timeout/cancel; instance-owned `last_write` / `last_pin` / `take_window` meters (a second engine's window stays empty); txstat fee / size / weight from the load assemble, restamp and its refuses; same-run create then spend; 546-shaped 2-vout merge + same-block chain + cross-batch head resolve; an already-at-height retry finishes a spend annotate |
| `mempool_under_pressure` | Mempool + RPC (crate) | One entry in `orphanage`, `accept`, `tx_relay`, and `methods_tests`: orphan reserve and expiry, sigops before script, rolling fee floor, cluster cap, parked min-relay orphan, and the package RPC rejects (unsorted, missing inputs, conflict, min-relay parent with maxfeerate child). |
| `mempool_under_pressure` | Mempool + RPC (crate) | One entry in `orphanage`, `accept`, `tx_relay`, and `methods_tests`: orphan reserve and expiry, sigops before script, rolling fee floor, cluster cap, parked min-relay orphan, and the package RPC rejects (unsorted, missing inputs, conflict, min-relay parent with maxfeerate child). A three-tx ancestor package whose grandparent and mid are under the relay floor admits when the grandchild pays. Fee 501 at vsize 5000 and 5001 floors to the same sat/kvB; a tight weight cap evicts the lighter txid. |
| `sigop_adjusted_size_budget_and_reopen` | Mempool (crate) | One empty `ActiveMempool`: sigop-adjusted vsize (boundary, min relay, full-pool floor, RBF, package and 1p1c), the raw-weight cluster limit, the shared block sigop budget, and sigop cost plus bytes-per-sigop and reserve overlays across reopen and compact. Each beat restores an empty pool. |
| `mempool_accept_life` | Mempool (crate) | One `ActiveMempool` against one chain view that blocks move. Orphan parks and re-announce, dry run not parked, parent promotes the child; missing vout, invalid parent, and block-spent coin reject without parking. Full RBF and no return, the staged commit failing closed on a conflict that landed after prepare, pure RBFR unpinning a child, replaced txs out of the cluster count; a ~30 kvB single tx under the vsize cap and the ten-way merge over it. Package order, CPFP, child fail restoring the RBF victim. A block evicts double-spent txs with descendants; a reorg readmits the parent and evicts the BIP68 and coinbase-maturity spends. Raised `-minrelaytxfee`: 1p1c needs a paying child, an unrelated tx does not ride the waiver, child fail takes a promoted spender down. Full pool: a protected lone worst chunk evicts nothing and leaves the floor, the next arrival evicts the CPFP pair together. |
| `rpc_regtest_from_genesis` | RPC (crate) | One regtest hub from genesis through the first submits. At genesis: `size_on_disk` is the store walk, IBD comes from the hub and not the stale atomic, buried deployments, `generateblock submit=false` connects nothing, and the priority, mocktime, mockscheduler, submitheader decode, and not-found refuses. The first block pays a p2wpkh address: display-order hashes and txids, raw `getblock` and header, a headers-only child at progress 0.5. Mocktime stamps `generate` and makes a far block `time-too-new`. Coinbase-only blocks: verbosity 1 without a seqsigwit zip, `getnetworkhashps` over chainwork, the empty template and proposal needles, a `time-too-old` header, an invalid parent body that marks its branch, and the structure rejects named on this hub in [`docs/consensus-tests.md`](docs/consensus-tests.md) (S1–S3, S6's RPC `bad-txnmrklroot`, S8's missing and mismatched witness commitment, S9–S11, S13, S17, S18), including S1–S3 on an equal-work sibling. A resubmit of a rejected sibling is `duplicate-invalid`; a valid sibling stays `inconclusive`. |
| `rpc_regtest_mature_chain_ops` | RPC (crate) | One regtest hub mined to height 130. The `nblocks=0` window, GBT fee and sigops (bare, P2SH, P2WSH), sigop-adjusted mempool vsize (`getmempoolentry`, package retry, `blockmintxfee`; weight and the Esplora/Electrum histogram stay raw) and a big-sigops cluster under the block budget, a non-DER spend with Core `reject-details`, deprioritise, `generateblock` reject shapes then parent-first mining, a premature coinbase, proposal spend/value/final needles against the chain, a repeated txid and a second coinbase, default and explicit `maxfeerate`, `testmempoolaccept` known vs mempool vs archived, invalidate and reconsider, and a parked sibling (held `getblock`, `preciousblock`). Last, a mainnet view of the same hub refuses the regtest-only methods and still takes `submitblock`. |
| `rpc_regtest_mature_chain_ops` | RPC (crate) | One regtest hub mined to height 130. The `nblocks=0` window, GBT fee and sigops (bare, P2SH, P2WSH), sigop-adjusted mempool vsize (`getmempoolentry`, package retry, `blockmintxfee`; weight and the Esplora/Electrum histogram stay raw) and a big-sigops cluster under the block budget, a non-DER spend with Core `reject-details`, deprioritise, `getblocktemplate` order (higher rate, then higher fee, then the lesser txid, and a negative fee delta omitted), `generateblock` reject shapes then parent-first mining, a premature coinbase, proposal spend/value/final needles against the chain, a repeated txid and a second coinbase, default and explicit `maxfeerate`, `testmempoolaccept` known vs mempool vs archived, invalidate and reconsider, and a parked sibling (held `getblock`, `preciousblock`). Last, a mainnet view of the same hub refuses the regtest-only methods and still takes `submitblock`. |
| `block_cache_and_mempool_hub_surface` | Net | BlockCache locator/eviction + MempoolHub accept/remove/reorg on mature chain. Eviction uses body depth 16. |
| `store_error_and_corrupt_paths` | Store | Error/corrupt surfaces |
| `store_table_header_and_idx_corrupt` | Store | Table header/head corrupt open |
Expand Down
5 changes: 5 additions & 0 deletions changelog.d/overnight-mutant-pins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Changed

- Overnight missed mutants in confirm, logging, and the mempool are pinned or removed where the operator could not change a block, a peer message, or a reopened pool.
- Confirm progress, parent checks, and reject rewinds are pinned on the paths the confirm thread calls. Mempool compaction uses the dead-slot quarter, worst-chunk order keeps the earlier tied rate, eviction stops when a slot is free, and a package that spends a later txid is not topological.
- API-log truncation uses the standard char boundary, so a cut in the middle of a character cannot spin.
63 changes: 39 additions & 24 deletions crates/rbitcoin-log/src/api_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,7 @@ fn compact_params(params: &str) -> String {
if params.len() <= PARAMS_MAX {
return params.to_string();
}
let mut end = PARAMS_MAX;
while end > 0 && !params.is_char_boundary(end) {
end -= 1;
}
format!("{}…", &params[..end])
format!("{}…", &params[..char_floor(params, PARAMS_MAX)])
}

/// Record one Electrum / Esplora / RPC call.
Expand Down Expand Up @@ -113,11 +109,7 @@ fn logged_params(method: &str, params: &str) -> String {
}

fn char_floor(s: &str, max: usize) -> usize {
let mut end = max.min(s.len());
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
end
s.floor_char_boundary(max.min(s.len()))
}

/// Exclusive end of the slice to redact, covering each secret that starts
Expand Down Expand Up @@ -245,15 +237,6 @@ mod tests {

static TEST_LOCK: Mutex<()> = Mutex::new(());

#[test]
fn compact_params_truncates() {
assert_eq!(compact_params("[]"), "[]");
let long = "x".repeat(500);
let c = compact_params(&long);
assert!(c.ends_with('…'));
assert!(c.len() < 400);
}

#[test]
fn api_log_file_records_json_line() {
let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
Expand Down Expand Up @@ -283,7 +266,21 @@ mod tests {
1,
Some("unknown method: no.such"),
);
// 383 ASCII bytes, then a 3-byte character that straddles byte 384.
// The operator file keeps the char-safe prefix and drops the tail.
let mut straddling = "a".repeat(383);
straddling.push('€');
straddling.push_str("zzz");
api_call("rpc", "-", "long.params", &straddling, 2, None);
api_call("rpc", "-", "odd.params", "a\"b\\c\n\r\t\u{0001}", 3, None);
close_api_log();
let sealed = std::fs::metadata(&path).unwrap().len();
api_call("electrum", "127.0.0.1:1", "server.ping", "[]", 1, None);
assert_eq!(
std::fs::metadata(&path).unwrap().len(),
sealed,
"close stops the operator file"
);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
Expand All @@ -299,6 +296,29 @@ mod tests {
assert!(body.contains("\"ok\":true"));
assert!(body.contains("\"ok\":false"));
assert!(body.contains("unknown method"));
assert!(body.contains(&format!("\"params\":\"{}…\"", "a".repeat(383))));
assert!(!body.contains('€'), "{body}");
assert!(!body.contains("zzz"), "{body}");
assert!(body.contains(r#""params":"a\"b\\c\n\r\t\u0001""#), "{body}");
}

#[test]
fn init_api_log_creates_a_missing_parent() {
let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let root = std::env::temp_dir().join(format!(
"rbitcoin-api-parent-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
let path = root.join("nested").join("api.jsonl");
let _ = std::fs::remove_dir_all(&root);
init_api_log(&path).unwrap();
close_api_log();
assert!(path.is_file(), "operator path must exist");
let _ = std::fs::remove_dir_all(&root);
}

#[test]
Expand All @@ -314,11 +334,6 @@ mod tests {
assert!(msg.contains("server.ping"), "{msg}");
}

#[test]
fn json_escape_quotes() {
assert_eq!(json_escape("a\"b\\c"), "a\\\"b\\\\c");
}

#[test]
fn api_call_redacts_scan_secrets_and_ext_privkeys() {
let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
Expand Down
46 changes: 46 additions & 0 deletions crates/rbitcoin-log/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,12 @@ mod tests {
// 2024-01-01 00:00:00 UTC
let s = format_timestamp(UNIX_EPOCH + std::time::Duration::from_secs(1_704_067_200));
assert_eq!(s, "2024-01-01T00:00:00.000Z");
// February and December are the Hinnant month arms. A log line's
// timestamp is the operator surface; these two dates are the arms.
let s = format_timestamp(UNIX_EPOCH + std::time::Duration::from_secs(31 * 86_400));
assert_eq!(s, "1970-02-01T00:00:00.000Z");
let s = format_timestamp(UNIX_EPOCH + std::time::Duration::from_secs(364 * 86_400));
assert_eq!(s, "1970-12-31T00:00:00.000Z");
}

#[test]
Expand Down Expand Up @@ -422,4 +428,44 @@ mod tests {
log_at_style(Level::Error, Style::Bold, format_args!("bold-err"));
init(Level::Info);
}

// Operator stderr, not the capture buffer. A redirected fd is the log file
// an operator tails. Unix-only: the logger writes fd 2.
#[cfg(unix)]
unsafe extern "C" {
safe fn dup(fd: i32) -> i32;
safe fn dup2(oldfd: i32, newfd: i32) -> i32;
safe fn close(fd: i32) -> i32;
}

#[cfg(unix)]
#[test]
fn stderr_line_is_plain_when_not_a_terminal() {
let _g = lock_log_init();
init(Level::Error);
let path = std::env::temp_dir().join(format!(
"rbitcoin-log-stderr-{}-{}.txt",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
let file = std::fs::File::create(&path).unwrap();
use std::os::fd::AsRawFd;
let saved = dup(2);
assert!(saved >= 0);
assert_eq!(dup2(file.as_raw_fd(), 2), 2);
log_at_style(Level::Error, Style::Bold, format_args!("operator-line"));
log_at_style(Level::Info, Style::Plain, format_args!("hidden-info"));
let _ = io::stderr().flush();
assert_eq!(dup2(saved, 2), 2);
close(saved);
let body = std::fs::read_to_string(&path).unwrap();
let _ = std::fs::remove_file(&path);
init(Level::Info);
assert!(body.contains("operator-line"), "{body}");
assert!(!body.contains('\u{1b}'), "{body}");
assert!(!body.contains("hidden-info"), "{body}");
}
}
Loading
Loading