From 055ac9f4ab88aef71f9d9451dfbfaeb8563d185d Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 23 Mar 2024 09:08:45 +0000 Subject: [PATCH 1/5] Add testmempoolaccept endpoint --- src/daemon.rs | 26 ++++++++++++++++++++++++++ src/new_index/query.rs | 6 +++++- src/rest.rs | 12 ++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/daemon.rs b/src/daemon.rs index b794a1f9..01215a59 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -117,6 +117,26 @@ struct NetworkInfo { relayfee: f64, // in BTC/kB } +#[derive(Serialize, Deserialize, Debug)] +struct MempoolFees { + base: f64, + #[serde(rename = "effective-feerate")] + effective_feerate: f64, + #[serde(rename = "effective-includes")] + effective_includes: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct MempoolAcceptResult { + txid: String, + wtxid: String, + allowed: Option, + vsize: Option, + fees: Option, + #[serde(rename = "reject-reason")] + reject_reason: Option, +} + pub trait CookieGetter: Send + Sync { fn get(&self) -> Result>; } @@ -582,6 +602,12 @@ impl Daemon { .chain_err(|| "failed to parse txid") } + pub fn test_mempool_accept(&self, txhex: Vec) -> Result> { + let result = self.request("testmempoolaccept", json!([txhex]))?; + serde_json::from_value::>(result) + .chain_err(|| "invalid testmempoolaccept reply") + } + // Get estimated feerates for the provided confirmation targets using a batch RPC request // Missing estimates are logged but do not cause a failure, whatever is available is returned #[allow(clippy::float_cmp)] diff --git a/src/new_index/query.rs b/src/new_index/query.rs index 3003a256..f7d2c78d 100644 --- a/src/new_index/query.rs +++ b/src/new_index/query.rs @@ -6,7 +6,7 @@ use std::time::{Duration, Instant}; use crate::chain::{Network, OutPoint, Transaction, TxOut, Txid}; use crate::config::Config; -use crate::daemon::Daemon; +use crate::daemon::{Daemon, MempoolAcceptResult}; use crate::errors::*; use crate::new_index::{ChainQuery, Mempool, ScriptStats, SpendingInput, Utxo}; use crate::util::{is_spendable, BlockId, Bytes, TransactionStatus}; @@ -87,6 +87,10 @@ impl Query { Ok(txid) } + pub fn test_mempool_accept(&self, txhex: Vec) -> Result> { + self.daemon.test_mempool_accept(txhex) + } + pub fn utxo(&self, scripthash: &[u8]) -> Result> { let mut utxos = self.chain.utxo( scripthash, diff --git a/src/rest.rs b/src/rest.rs index df085441..f75f34a0 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -1202,6 +1202,18 @@ fn handle_request( .map_err(|err| HttpError::from(err.description().to_string()))?; http_message(StatusCode::OK, txid.to_hex(), 0) } + (&Method::POST, Some(&"txs"), Some(&"test"), None, None, None) => { + let txhexes: Vec = String::from_utf8(body.to_vec())? + .split(',') + .map(|s| s.to_string()) + .collect(); + + let result = query + .test_mempool_accept(txhexes) + .map_err(|err| HttpError::from(err.description().to_string()))?; + + json_response(result, TTL_SHORT) + } (&Method::GET, Some(&"txs"), Some(&"outspends"), None, None, None) => { let txid_strings: Vec<&str> = query_params .get("txids") From 946ea714eda165d6c77e5066ef8bfefff9c24622 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 24 Mar 2024 05:30:24 +0000 Subject: [PATCH 2/5] testmempoolaccept add maxfeerate param --- src/daemon.rs | 12 ++++++++++-- src/new_index/query.rs | 8 ++++++-- src/rest.rs | 9 ++++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 01215a59..7d4dafa3 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -602,8 +602,16 @@ impl Daemon { .chain_err(|| "failed to parse txid") } - pub fn test_mempool_accept(&self, txhex: Vec) -> Result> { - let result = self.request("testmempoolaccept", json!([txhex]))?; + pub fn test_mempool_accept( + &self, + txhex: Vec, + maxfeerate: Option, + ) -> Result> { + let params = match maxfeerate { + Some(rate) => json!([txhex, format!("{:.8}", rate)]), + None => json!([txhex]), + }; + let result = self.request("testmempoolaccept", params)?; serde_json::from_value::>(result) .chain_err(|| "invalid testmempoolaccept reply") } diff --git a/src/new_index/query.rs b/src/new_index/query.rs index f7d2c78d..c64cd6f9 100644 --- a/src/new_index/query.rs +++ b/src/new_index/query.rs @@ -87,8 +87,12 @@ impl Query { Ok(txid) } - pub fn test_mempool_accept(&self, txhex: Vec) -> Result> { - self.daemon.test_mempool_accept(txhex) + pub fn test_mempool_accept( + &self, + txhex: Vec, + maxfeerate: Option, + ) -> Result> { + self.daemon.test_mempool_accept(txhex, maxfeerate) } pub fn utxo(&self, scripthash: &[u8]) -> Result> { diff --git a/src/rest.rs b/src/rest.rs index f75f34a0..cb620d4a 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -1207,9 +1207,16 @@ fn handle_request( .split(',') .map(|s| s.to_string()) .collect(); + let maxfeerate = query_params + .get("maxfeerate") + .map(|s| { + s.parse::() + .map_err(|_| HttpError::from("Invalid maxfeerate".to_string())) + }) + .transpose()?; let result = query - .test_mempool_accept(txhexes) + .test_mempool_accept(txhexes, maxfeerate) .map_err(|err| HttpError::from(err.description().to_string()))?; json_response(result, TTL_SHORT) From 569af75849cc9e359325b0ba25606e4c6310de95 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 24 Mar 2024 06:34:15 +0000 Subject: [PATCH 3/5] Add testmempoolaccept pre-checks --- src/rest.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/rest.rs b/src/rest.rs index cb620d4a..5ab38b7a 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -1215,6 +1215,19 @@ fn handle_request( }) .transpose()?; + // pre-checks + txhexes.iter().try_for_each(|txhex| { + // each transaction must be of reasonable size (more than 60 bytes, within 400kWU standardness limit) + if !(120..800_000).contains(&txhex.len()) { + Result::Err(HttpError::from("Invalid transaction size".to_string())) + } else { + // must be a valid hex string + Vec::::from_hex(txhex) + .map_err(|_| HttpError::from("Invalid transaction hex".to_string())) + .map(|_| ()) + } + })?; + let result = query .test_mempool_accept(txhexes, maxfeerate) .map_err(|err| HttpError::from(err.description().to_string()))?; From f7385ce392472e3cc6e49a3bac72b1b0cf298c1a Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 25 Mar 2024 05:03:38 +0000 Subject: [PATCH 4/5] testmempoolaccept JSON input, tx limit, error indices --- src/rest.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/rest.rs b/src/rest.rs index 5ab38b7a..71478312 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -1203,10 +1203,15 @@ fn handle_request( http_message(StatusCode::OK, txid.to_hex(), 0) } (&Method::POST, Some(&"txs"), Some(&"test"), None, None, None) => { - let txhexes: Vec = String::from_utf8(body.to_vec())? - .split(',') - .map(|s| s.to_string()) - .collect(); + let txhexes: Vec = + serde_json::from_str(String::from_utf8(body.to_vec())?.as_str())?; + + if txhexes.len() > 25 { + Result::Err(HttpError::from( + "Exceeded maximum of 25 transactions".to_string(), + ))? + } + let maxfeerate = query_params .get("maxfeerate") .map(|s| { @@ -1216,14 +1221,19 @@ fn handle_request( .transpose()?; // pre-checks - txhexes.iter().try_for_each(|txhex| { + txhexes.iter().enumerate().try_for_each(|(index, txhex)| { // each transaction must be of reasonable size (more than 60 bytes, within 400kWU standardness limit) if !(120..800_000).contains(&txhex.len()) { - Result::Err(HttpError::from("Invalid transaction size".to_string())) + Result::Err(HttpError::from(format!( + "Invalid transaction size for item {}", + index + ))) } else { // must be a valid hex string Vec::::from_hex(txhex) - .map_err(|_| HttpError::from("Invalid transaction hex".to_string())) + .map_err(|_| { + HttpError::from(format!("Invalid transaction hex for item {}", index)) + }) .map(|_| ()) } })?; From ac32e4b1c3053c4a86fe4415e062ff48a1e5775e Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 25 Mar 2024 05:54:33 +0000 Subject: [PATCH 5/5] testmempoolaccept maxfeerate f32 -> f64 --- src/daemon.rs | 2 +- src/new_index/query.rs | 2 +- src/rest.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 7d4dafa3..254c168e 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -605,7 +605,7 @@ impl Daemon { pub fn test_mempool_accept( &self, txhex: Vec, - maxfeerate: Option, + maxfeerate: Option, ) -> Result> { let params = match maxfeerate { Some(rate) => json!([txhex, format!("{:.8}", rate)]), diff --git a/src/new_index/query.rs b/src/new_index/query.rs index c64cd6f9..3e314fd1 100644 --- a/src/new_index/query.rs +++ b/src/new_index/query.rs @@ -90,7 +90,7 @@ impl Query { pub fn test_mempool_accept( &self, txhex: Vec, - maxfeerate: Option, + maxfeerate: Option, ) -> Result> { self.daemon.test_mempool_accept(txhex, maxfeerate) } diff --git a/src/rest.rs b/src/rest.rs index 71478312..eab06de5 100644 --- a/src/rest.rs +++ b/src/rest.rs @@ -1215,7 +1215,7 @@ fn handle_request( let maxfeerate = query_params .get("maxfeerate") .map(|s| { - s.parse::() + s.parse::() .map_err(|_| HttpError::from("Invalid maxfeerate".to_string())) }) .transpose()?;