Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}));
}
},
};
Comment thread
Pantamis marked this conversation as 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()
}
}