diff --git a/src/electrum.rs b/src/electrum.rs index ba182bb07..4b16d5cf8 100644 --- a/src/electrum.rs +++ b/src/electrum.rs @@ -2,8 +2,9 @@ use anyhow::{bail, Context, Result}; use bitcoin::{ consensus::{deserialize, serialize}, hashes::hex::{FromHex, ToHex}, - BlockHash, Txid, + BlockHash, OutPoint, Transaction, Txid, }; +use bitcoincore_rpc::jsonrpc; use rayon::prelude::*; use serde_derive::Deserialize; use serde_json::{self, json, Value}; @@ -275,6 +276,74 @@ impl Rpc { Ok(status) } + fn outpoint_subscribe(&self, (txid, vout): (Txid, u32)) -> Result { + let funding = OutPoint { txid, vout }; + + let funding_blockhash = self.tracker.get_blockhash_by_txid(funding.txid); + let spending_blockhash = self.tracker.get_blockhash_spending_by_outpoint(funding); + + let funding_tx = match self.daemon.get_transaction(&funding.txid, funding_blockhash) { + Ok(tx) => tx, + Err(error) => { + match error.downcast_ref::() { + Some(bitcoincore_rpc::Error::JsonRpc(jsonrpc::Error::Rpc(jsonrpc::error::RpcError { code: -5, .. }))) => return Ok(json!({})), + _ => return Err(error), + } + }, + }; + let funding_inputs = &funding_tx.input; + let funding_height = match &funding_blockhash { + Some(funding_blockhash) => self.tracker.chain().get_block_height(funding_blockhash).ok_or_else(|| anyhow::anyhow!("Blockhash not found"))?, + None => 0, + }; + + let tx_candidates: Vec = match spending_blockhash { + None => self.tracker.mempool().filter_by_spending(&funding).iter().map(|e| e.txid).collect(), + Some(spending_blockhash) => { + let mut txids: Vec = Vec::new(); + self.daemon.for_blocks(Some(spending_blockhash).into_iter(), |_, block| { + let iter = block.txdata.into_iter().filter(|tx| is_spending(&tx, funding)).map(|tx| tx.txid()); + txids.extend(iter); + })?; + txids + }, + }; + + let mut spender_txids = tx_candidates.iter(); + + let spender_txid = spender_txids.next(); + let double_spending_txid = spender_txids.next(); // slice-based operator is fused, so this is OK + + let funding_inputs_confirmed = !(funding_blockhash.is_none() && funding_inputs.iter().any(|txi| self.tracker.get_blockhash_by_txid(txi.previous_output.txid).is_none())); + match (spender_txid, double_spending_txid, spending_blockhash) { + (Some(spender_txid), Some(double_spending_txid), _) => bail!("double spend of {}: {}", spender_txid, double_spending_txid), + (None, _, Some(spending_blockhash)) => bail!("Spending transaction {} wrongly indexed in block {}", funding.txid, spending_blockhash), + (Some(spender_txid), None, Some(spending_blockhash)) => { + let spending_height = self.tracker.chain().get_block_height(&spending_blockhash).ok_or_else(|| anyhow::anyhow!("Blockhash not found"))?; + return Ok(json!({"height": funding_height, "spender_txhash": spender_txid, "spender_height": spending_height})); + }, + (Some(spender_txid), None, None) => { + let spending_tx = self.daemon.get_transaction(&spender_txid, None)?; + if funding_inputs_confirmed { + if spending_tx.input.iter().any(|txi| self.tracker.get_blockhash_by_txid(txi.previous_output.txid).is_none()) { + return Ok(json!({"height": funding_height, "spender_txhash": spender_txid, "spender_height": -1})); + } else { + return Ok(json!({"height": funding_height, "spender_txhash": spender_txid, "spender_height": 0})); + } + } else { + return Ok(json!({"height": -1, "spender_txhash": spender_txid, "spender_height": -1})); + } + }, + (None, _, None) => { + if funding_inputs_confirmed { + return Ok(json!({"height": funding_height})); + } else { + return Ok(json!({"height": -1})); + } + }, + }; + } + fn transaction_broadcast(&self, (tx_hex,): (String,)) -> Result { let tx_bytes = Vec::from_hex(&tx_hex).context("non-hex transaction")?; let tx = deserialize(&tx_bytes).context("invalid transaction")?; @@ -384,6 +453,7 @@ impl Rpc { Call::Donation => Ok(Value::Null), Call::EstimateFee(args) => self.estimate_fee(args), Call::HeadersSubscribe => self.headers_subscribe(client), + Call::OutpointSubscribe(args) => self.outpoint_subscribe(args), Call::MempoolFeeHistogram => self.get_fee_histogram(), Call::PeersSubscribe => Ok(json!([])), Call::Ping => Ok(Value::Null), @@ -423,6 +493,7 @@ enum Call { EstimateFee((u16,)), HeadersSubscribe, MempoolFeeHistogram, + OutpointSubscribe((Txid, u32)), PeersSubscribe, Ping, RelayFee, @@ -441,6 +512,7 @@ impl Call { "blockchain.block.headers" => Call::BlockHeaders(convert(params)?), "blockchain.estimatefee" => Call::EstimateFee(convert(params)?), "blockchain.headers.subscribe" => Call::HeadersSubscribe, + "blockchain.outpoint.subscribe" => Call::OutpointSubscribe(convert(params)?), "blockchain.relayfee" => Call::RelayFee, "blockchain.scripthash.get_balance" => Call::ScriptHashGetBalance(convert(params)?), "blockchain.scripthash.get_history" => Call::ScriptHashGetHistory(convert(params)?), @@ -484,3 +556,8 @@ fn result_msg(id: Value, result: Value) -> Value { fn error_msg(id: Value, error: RpcError) -> Value { json!({"jsonrpc": "2.0", "id": id, "error": error.to_value()}) } + +fn is_spending(tx: &Transaction, funding: OutPoint) -> bool { + tx.input.iter().any(|txi| txi.previous_output == funding) +} + diff --git a/src/tracker.rs b/src/tracker.rs index 6574b4b11..2dff02c5a 100644 --- a/src/tracker.rs +++ b/src/tracker.rs @@ -44,6 +44,10 @@ impl Tracker { self.index.chain() } + pub(crate) fn mempool(&self) -> &Mempool { + &self.mempool + } + pub(crate) fn fees_histogram(&self) -> &Histogram { self.mempool.fees_histogram() } @@ -101,4 +105,8 @@ impl Tracker { // Note: there are two blocks with coinbase transactions having same txid (see BIP-30) self.index.filter_by_txid(txid).next() } + + pub fn get_blockhash_spending_by_outpoint(&self, funding: OutPoint) -> Option { + self.index.filter_by_spending(funding).next() + } }