Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP_DO_NOT_MERGE] Add outpoint subscribe method #446

Closed
wants to merge 10 commits into from
79 changes: 78 additions & 1 deletion src/electrum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -275,6 +276,74 @@ impl Rpc {
Ok(status)
}

fn outpoint_subscribe(&self, (txid, vout): (Txid, u32)) -> Result<Value> {
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::<bitcoincore_rpc::Error>() {
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<Txid> = match spending_blockhash {
None => self.tracker.mempool().filter_by_spending(&funding).iter().map(|e| e.txid).collect(),
Some(spending_blockhash) => {
let mut txids: Vec<Txid> = 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}));
}
},
};
Pantamis marked this conversation as resolved.
Show resolved Hide resolved
}

fn transaction_broadcast(&self, (tx_hex,): (String,)) -> Result<Value> {
let tx_bytes = Vec::from_hex(&tx_hex).context("non-hex transaction")?;
let tx = deserialize(&tx_bytes).context("invalid transaction")?;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -423,6 +493,7 @@ enum Call {
EstimateFee((u16,)),
HeadersSubscribe,
MempoolFeeHistogram,
OutpointSubscribe((Txid, u32)),
PeersSubscribe,
Ping,
RelayFee,
Expand All @@ -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)?),
Expand Down Expand Up @@ -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)
}

8 changes: 8 additions & 0 deletions src/tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down Expand Up @@ -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<BlockHash> {
self.index.filter_by_spending(funding).next()
}
}