diff --git a/changelog.d/sh-cap-message.md b/changelog.d/sh-cap-message.md new file mode 100644 index 000000000..99062c491 --- /dev/null +++ b/changelog.d/sh-cap-message.md @@ -0,0 +1,7 @@ +Fixed + +- **The `--max-sh-creates` refusal names the real numbers.** It read + `(default 10000)` whatever the configured cap was. It now reports the + scripthash's create count and the cap in force, for example + `scripthash join exceeds --max-sh-creates (14437 creates, cap 10000)`. + Esplora still answers 503. diff --git a/crates/rbitcoin-esplora/src/esplora_sh_journey.rs b/crates/rbitcoin-esplora/src/esplora_sh_journey.rs index 12940a1ac..401be1142 100644 --- a/crates/rbitcoin-esplora/src/esplora_sh_journey.rs +++ b/crates/rbitcoin-esplora/src/esplora_sh_journey.rs @@ -6,7 +6,7 @@ async fn cap_refuses_three_creates(q: &Query, app: &Router) { let (st, body) = oneshot_http(app, get_with_client(&format!("/scripthash/{h51}"), "cap")).await; assert_eq!(st, 503, "{body}"); assert!( - body.contains("scripthash join exceeds --max-sh-creates"), + body.contains("scripthash join exceeds --max-sh-creates (3 creates, cap 2)"), "{body}" ); let (st, utxo) = diff --git a/crates/rbitcoin-esplora/src/server.rs b/crates/rbitcoin-esplora/src/server.rs index 54b65f9f5..9b7f3a95c 100644 --- a/crates/rbitcoin-esplora/src/server.rs +++ b/crates/rbitcoin-esplora/src/server.rs @@ -1130,6 +1130,9 @@ pub(crate) fn store_err(e: rbitcoin_query::QueryError) -> Response { StoreError::Pruned { .. } => (StatusCode::NOT_FOUND, "pruned").into_response(), StoreError::Stale(m) => (StatusCode::SERVICE_UNAVAILABLE, m).into_response(), StoreError::Rejected(m) => (StatusCode::SERVICE_UNAVAILABLE, m).into_response(), + e @ StoreError::ShCreatesCap { .. } => { + (StatusCode::SERVICE_UNAVAILABLE, e.to_string()).into_response() + } other => (StatusCode::INTERNAL_SERVER_ERROR, other.to_string()).into_response(), } } diff --git a/crates/rbitcoin-query/src/lib.rs b/crates/rbitcoin-query/src/lib.rs index 80260f6fb..eceb9412a 100644 --- a/crates/rbitcoin-query/src/lib.rs +++ b/crates/rbitcoin-query/src/lib.rs @@ -1306,9 +1306,6 @@ impl Query { self.reconstruct_archived.swap(0, AtomicOrdering::Relaxed) } - pub const MAX_SH_CREATES_MSG: &'static str = - "scripthash join exceeds --max-sh-creates (default 10000)"; - /// [`StoreError::Rejected`] payload from [`Self::block_queue_offer`] for /// wire that does not decode. Only this refusal is the sender's fault. pub const UNDECODABLE_WIRE_MSG: &'static str = "block wire does not decode"; diff --git a/crates/rbitcoin-query/src/query_sh_caps_journey.rs b/crates/rbitcoin-query/src/query_sh_caps_journey.rs index 09df260d9..e811026e5 100644 --- a/crates/rbitcoin-query/src/query_sh_caps_journey.rs +++ b/crates/rbitcoin-query/src/query_sh_caps_journey.rs @@ -102,7 +102,7 @@ fn sh_history_caps() { q.scripthash_history(&sh).map(|_| ()).unwrap_err(), ] { assert!( - matches!(err, StoreError::Rejected(m) if m == Query::MAX_SH_CREATES_MSG), + err.to_string() == "scripthash join exceeds --max-sh-creates (7 creates, cap 2)", "{err}" ); } @@ -157,7 +157,7 @@ fn sh_history_caps() { ) .unwrap_err(); assert!( - matches!(err, StoreError::Rejected(m) if m == Query::MAX_SH_CREATES_MSG), + err.to_string() == "scripthash join exceeds --max-sh-creates (7 creates, cap 2)", "{err}" ); diff --git a/crates/rbitcoin-query/src/scripthash.rs b/crates/rbitcoin-query/src/scripthash.rs index 980591e33..b1e3c8602 100644 --- a/crates/rbitcoin-query/src/scripthash.rs +++ b/crates/rbitcoin-query/src/scripthash.rs @@ -602,7 +602,7 @@ impl Query { if cap > 0 && !paging { let n = self.scripthash_create_count(scripthash)?; if n > cap { - return Err(StoreError::Rejected(Query::MAX_SH_CREATES_MSG)); + return Err(StoreError::ShCreatesCap { creates: n, cap }); } } let t_pages = std::time::Instant::now(); diff --git a/crates/rbitcoin-store/src/error.rs b/crates/rbitcoin-store/src/error.rs index 26a0c4022..5dd709ef0 100644 --- a/crates/rbitcoin-store/src/error.rs +++ b/crates/rbitcoin-store/src/error.rs @@ -31,6 +31,11 @@ pub enum StoreError { Stale(&'static str), /// Request refused (DoS cap) — not on-disk corruption. Rejected(&'static str), + /// Unpaged scripthash join over `--max-sh-creates` (DoS cap) — not on-disk corruption. + ShCreatesCap { + creates: u32, + cap: u32, + }, /// Class A seqsigwit dropped at/below the prune watermark. Not corruption. Pruned { height: u32, @@ -95,6 +100,10 @@ impl fmt::Display for StoreError { StoreError::Layout(m) => write!(f, "{m}"), StoreError::Stale(m) => write!(f, "{m}"), StoreError::Rejected(m) => f.write_str(m), + StoreError::ShCreatesCap { creates, cap } => write!( + f, + "scripthash join exceeds --max-sh-creates ({creates} creates, cap {cap})" + ), StoreError::Pruned { height } => write!(f, "pruned data at height {height}"), } } @@ -144,8 +153,12 @@ mod tests { StoreError::Unavailable, StoreError::Layout("seqsigwit is on a cold datadir".into()), StoreError::Stale("chain view moved"), - StoreError::Rejected("scripthash join exceeds --max-sh-creates (default 10000)"), + StoreError::Rejected("block wire does not decode"), StoreError::Pruned { height: 12 }, + StoreError::ShCreatesCap { + creates: 14437, + cap: 10000, + }, ]; let texts: Vec = arms.iter().map(|e| e.to_string()).collect(); assert_eq!(texts[0], "invalid store magic"); @@ -161,11 +174,12 @@ mod tests { assert_eq!(texts[9], "io_uring unavailable"); assert_eq!(texts[10], "seqsigwit is on a cold datadir"); assert_eq!(texts[11], "chain view moved"); + assert_eq!(texts[12], "block wire does not decode"); + assert_eq!(texts[13], "pruned data at height 12"); assert_eq!( - texts[12], - "scripthash join exceeds --max-sh-creates (default 10000)" + texts[14], + "scripthash join exceeds --max-sh-creates (14437 creates, cap 10000)" ); - assert_eq!(texts[13], "pruned data at height 12"); for e in &arms { assert!(e.source().is_none()); } diff --git a/docs/operator/storage.md b/docs/operator/storage.md index 31f117953..10b265236 100644 --- a/docs/operator/storage.md +++ b/docs/operator/storage.md @@ -151,7 +151,7 @@ Electrum/Esplora **start without** `--sh-index`. Address/scripthash methods then `--max-sh-creates N` (conf `max_sh_creates=`) defaults to **10000**. An unpaged Electrum history or full stats join with more than N creates is refused before Class A expand: Esplora **503** / Electrum JSON-RPC error -`scripthash join exceeds --max-sh-creates (default 10000)`. **0** is unlimited. +`scripthash join exceeds --max-sh-creates ( creates, cap )`. **0** is unlimited. A history request that names a page (Esplora's 25, or any caller that sets a limit) is still served: the join stops once that page is full.