From e882b368868010c201ac9bb5990f09ee9208c70a Mon Sep 17 00:00:00 2001 From: Pantamis Date: Sat, 24 Jul 2021 13:10:07 +0200 Subject: [PATCH] Add outpoint subscribe method --- src/electrum.rs | 92 ++++++++++++++++++++++++++++++++++++++++++++++++- src/status.rs | 8 ++--- src/tracker.rs | 8 +++++ 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/electrum.rs b/src/electrum.rs index ba182bb07..919fdb2b6 100644 --- a/src/electrum.rs +++ b/src/electrum.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Context, Result}; use bitcoin::{ consensus::{deserialize, serialize}, hashes::hex::{FromHex, ToHex}, - BlockHash, Txid, + BlockHash, OutPoint, Transaction, Txid, }; use rayon::prelude::*; use serde_derive::Deserialize; @@ -275,6 +275,88 @@ 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 mut inputs_funding_confirmed = true; + + let funding_tx = self.daemon.get_transaction(&funding.txid, funding_blockhash); + + if funding_tx.is_err() { + return Ok(json!({})); + } + + let funding_inputs = &funding_tx.as_ref().unwrap().input; + let outpoint_script = &funding_tx.as_ref().unwrap().output.get(vout as usize).unwrap().script_pubkey; + + if funding_blockhash.is_none() && funding_inputs.iter().any(|txi| self.tracker.get_blockhash_by_txid(txi.previous_output.txid).is_none()) { + inputs_funding_confirmed = false; + } + + + let scripthash = ScriptHash::new(&outpoint_script); + let outpoint_scripthash_status = self.new_status(scripthash); + + let funding_height = self.tracker.chain().get_block_height(&funding_blockhash.unwrap()).unwrap(); + + if spending_blockhash.is_none() { + let txids: Vec = outpoint_scripthash_status.unwrap() + .get_mempool(&(self.tracker).mempool()) + .iter() + .filter_map(|tx_entry| { + let mempool_tx = self.daemon.get_transaction(&tx_entry.txid, None); + if is_spending(&mempool_tx.unwrap(), funding) { + Some(tx_entry.txid) + } else { + None + } + }) + .collect(); + if txids.len() > 1 { + return Ok(panic!("double spend of {}: {:?}", txid, txids)); + } + if !txids.is_empty() { + let spending_tx = self.daemon.get_transaction(&txids[0], spending_blockhash); + if spending_tx.unwrap().input.iter().any(|txi| self.tracker.get_blockhash_by_txid(txi.previous_output.txid).is_none()) { + if inputs_funding_confirmed { + return Ok(json!({"height": 0, "spender_txhash": txids[0], "spender_height": -1})); + } + return Ok(json!({"height": -1, "spender_txhash": txids[0], "spender_height": -1})); + } + return Ok(json!({"height": funding_height, "spender_txhash": txids[0], "spender_height": 0})); + } + return Ok(json!({"height": funding_height})); + } + + let spending_height = self.tracker.chain().get_block_height(&spending_blockhash.unwrap()).unwrap(); + + let txids: Vec = outpoint_scripthash_status.unwrap() + .get_confirmed(&self.tracker.chain()) + .iter() + .filter(|confirmed_entry| { + confirmed_entry.height == spending_height + }) + .filter_map(|script_block_entry| { + let script_block_tx = self.daemon.get_transaction(&script_block_entry.txid, spending_blockhash); + if is_spending(&script_block_tx.unwrap(), funding) { + Some(script_block_entry.txid) + } else { + None + } + }) + .collect(); + + if txids.len() > 1 { + return Ok(panic!("double spend of {}: {:?}", txid, txids)); + } + if txids.is_empty() { return Ok(json!({"WTF": "spending tx not found but exist ?"})); } + return Ok(json!({"height": funding_height, "spender_txhash": txids[0], "spender_height": spending_height})); + + } + 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 +466,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 +506,7 @@ enum Call { EstimateFee((u16,)), HeadersSubscribe, MempoolFeeHistogram, + OutpointSubscribe((Txid, u32)), PeersSubscribe, Ping, RelayFee, @@ -441,6 +525,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 +569,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/status.rs b/src/status.rs index d2bd588c5..5eb0e5586 100644 --- a/src/status.rs +++ b/src/status.rs @@ -46,8 +46,8 @@ impl TxEntry { } pub(crate) struct ConfirmedEntry { - txid: Txid, - height: usize, + pub txid: Txid, + pub height: usize, } impl ConfirmedEntry { @@ -62,8 +62,8 @@ impl ConfirmedEntry { } pub(crate) struct MempoolEntry { - txid: Txid, - has_unconfirmed_inputs: bool, + pub txid: Txid, + pub has_unconfirmed_inputs: bool, fee: Amount, } 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() + } }