mirror of
https://github.com/romanz/electrs.git
synced 2024-11-19 01:43:29 +01:00
Add blockchain.outpoint.subscribe RPC
This commit is contained in:
parent
41763a44b3
commit
834bbd70f8
@ -2,7 +2,7 @@ use anyhow::{bail, Context, Result};
|
|||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
consensus::{deserialize, encode::serialize_hex},
|
consensus::{deserialize, encode::serialize_hex},
|
||||||
hashes::hex::FromHex,
|
hashes::hex::FromHex,
|
||||||
BlockHash, Txid,
|
BlockHash, OutPoint, Txid,
|
||||||
};
|
};
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
@ -21,7 +21,7 @@ use crate::{
|
|||||||
merkle::Proof,
|
merkle::Proof,
|
||||||
metrics::{self, Histogram, Metrics},
|
metrics::{self, Histogram, Metrics},
|
||||||
signals::Signal,
|
signals::Signal,
|
||||||
status::ScriptHashStatus,
|
status::{OutPointStatus, ScriptHashStatus},
|
||||||
tracker::Tracker,
|
tracker::Tracker,
|
||||||
types::ScriptHash,
|
types::ScriptHash,
|
||||||
};
|
};
|
||||||
@ -36,6 +36,7 @@ const UNSUBSCRIBED_QUERY_MESSAGE: &str = "your wallet uses less efficient method
|
|||||||
pub struct Client {
|
pub struct Client {
|
||||||
tip: Option<BlockHash>,
|
tip: Option<BlockHash>,
|
||||||
scripthashes: HashMap<ScriptHash, ScriptHashStatus>,
|
scripthashes: HashMap<ScriptHash, ScriptHashStatus>,
|
||||||
|
outpoints: HashMap<OutPoint, OutPointStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@ -185,7 +186,25 @@ impl Rpc {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<Value>>>()
|
.collect::<Result<Vec<Value>>>()
|
||||||
.context("failed to update status")?;
|
.context("failed to update scripthash status")?;
|
||||||
|
|
||||||
|
notifications.extend(
|
||||||
|
client
|
||||||
|
.outpoints
|
||||||
|
.par_iter_mut()
|
||||||
|
.filter_map(|(outpoint, status)| -> Option<Result<Value>> {
|
||||||
|
match self.tracker.update_outpoint_status(status, &self.daemon) {
|
||||||
|
Ok(true) => Some(Ok(notification(
|
||||||
|
"blockchain.outpoint.subscribe",
|
||||||
|
&[json!([outpoint.txid, outpoint.vout]), json!(status)],
|
||||||
|
))),
|
||||||
|
Ok(false) => None, // outpoint status is the same
|
||||||
|
Err(e) => Some(Err(e)),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<Value>>>()
|
||||||
|
.context("failed to update scripthash status")?,
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(old_tip) = client.tip {
|
if let Some(old_tip) = client.tip {
|
||||||
let new_tip = self.tracker.chain().tip();
|
let new_tip = self.tracker.chain().tip();
|
||||||
@ -350,6 +369,28 @@ impl Rpc {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn outpoint_subscribe(&self, client: &mut Client, (txid, vout): (Txid, u32)) -> Result<Value> {
|
||||||
|
let outpoint = OutPoint::new(txid, vout);
|
||||||
|
Ok(match client.outpoints.entry(outpoint) {
|
||||||
|
Entry::Occupied(e) => json!(e.get()),
|
||||||
|
Entry::Vacant(e) => {
|
||||||
|
let outpoint = OutPoint::new(txid, vout);
|
||||||
|
let mut status = OutPointStatus::new(outpoint);
|
||||||
|
self.tracker
|
||||||
|
.update_outpoint_status(&mut status, &self.daemon)?;
|
||||||
|
json!(e.insert(status))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outpoint_unsubscribe(
|
||||||
|
&self,
|
||||||
|
client: &mut Client,
|
||||||
|
(txid, vout): (Txid, u32),
|
||||||
|
) -> Result<Value> {
|
||||||
|
Ok(json!(client.outpoints.remove(&OutPoint::new(txid, vout))))
|
||||||
|
}
|
||||||
|
|
||||||
fn new_status(&self, scripthash: ScriptHash) -> Result<ScriptHashStatus> {
|
fn new_status(&self, scripthash: ScriptHash) -> Result<ScriptHashStatus> {
|
||||||
let mut status = ScriptHashStatus::new(scripthash);
|
let mut status = ScriptHashStatus::new(scripthash);
|
||||||
self.tracker
|
self.tracker
|
||||||
@ -548,6 +589,8 @@ impl Rpc {
|
|||||||
Params::Features => self.features(),
|
Params::Features => self.features(),
|
||||||
Params::HeadersSubscribe => self.headers_subscribe(client),
|
Params::HeadersSubscribe => self.headers_subscribe(client),
|
||||||
Params::MempoolFeeHistogram => self.get_fee_histogram(),
|
Params::MempoolFeeHistogram => self.get_fee_histogram(),
|
||||||
|
Params::OutPointSubscribe(args) => self.outpoint_subscribe(client, *args),
|
||||||
|
Params::OutPointUnsubscribe(args) => self.outpoint_unsubscribe(client, *args),
|
||||||
Params::PeersSubscribe => Ok(json!([])),
|
Params::PeersSubscribe => Ok(json!([])),
|
||||||
Params::Ping => Ok(Value::Null),
|
Params::Ping => Ok(Value::Null),
|
||||||
Params::RelayFee => self.relayfee(),
|
Params::RelayFee => self.relayfee(),
|
||||||
@ -572,12 +615,13 @@ enum Params {
|
|||||||
Banner,
|
Banner,
|
||||||
BlockHeader((usize,)),
|
BlockHeader((usize,)),
|
||||||
BlockHeaders((usize, usize)),
|
BlockHeaders((usize, usize)),
|
||||||
TransactionBroadcast((String,)),
|
|
||||||
Donation,
|
Donation,
|
||||||
EstimateFee((u16,)),
|
EstimateFee((u16,)),
|
||||||
Features,
|
Features,
|
||||||
HeadersSubscribe,
|
HeadersSubscribe,
|
||||||
MempoolFeeHistogram,
|
MempoolFeeHistogram,
|
||||||
|
OutPointSubscribe((Txid, u32)), // TODO: support spk_hint
|
||||||
|
OutPointUnsubscribe((Txid, u32)),
|
||||||
PeersSubscribe,
|
PeersSubscribe,
|
||||||
Ping,
|
Ping,
|
||||||
RelayFee,
|
RelayFee,
|
||||||
@ -586,6 +630,7 @@ enum Params {
|
|||||||
ScriptHashListUnspent((ScriptHash,)),
|
ScriptHashListUnspent((ScriptHash,)),
|
||||||
ScriptHashSubscribe((ScriptHash,)),
|
ScriptHashSubscribe((ScriptHash,)),
|
||||||
ScriptHashUnsubscribe((ScriptHash,)),
|
ScriptHashUnsubscribe((ScriptHash,)),
|
||||||
|
TransactionBroadcast((String,)),
|
||||||
TransactionGet(TxGetArgs),
|
TransactionGet(TxGetArgs),
|
||||||
TransactionGetMerkle((Txid, usize)),
|
TransactionGetMerkle((Txid, usize)),
|
||||||
TransactionFromPosition((usize, usize, bool)),
|
TransactionFromPosition((usize, usize, bool)),
|
||||||
@ -599,6 +644,8 @@ impl Params {
|
|||||||
"blockchain.block.headers" => Params::BlockHeaders(convert(params)?),
|
"blockchain.block.headers" => Params::BlockHeaders(convert(params)?),
|
||||||
"blockchain.estimatefee" => Params::EstimateFee(convert(params)?),
|
"blockchain.estimatefee" => Params::EstimateFee(convert(params)?),
|
||||||
"blockchain.headers.subscribe" => Params::HeadersSubscribe,
|
"blockchain.headers.subscribe" => Params::HeadersSubscribe,
|
||||||
|
"blockchain.outpoint.subscribe" => Params::OutPointSubscribe(convert(params)?),
|
||||||
|
"blockchain.outpoint.unsubscribe" => Params::OutPointUnsubscribe(convert(params)?),
|
||||||
"blockchain.relayfee" => Params::RelayFee,
|
"blockchain.relayfee" => Params::RelayFee,
|
||||||
"blockchain.scripthash.get_balance" => Params::ScriptHashGetBalance(convert(params)?),
|
"blockchain.scripthash.get_balance" => Params::ScriptHashGetBalance(convert(params)?),
|
||||||
"blockchain.scripthash.get_history" => Params::ScriptHashGetHistory(convert(params)?),
|
"blockchain.scripthash.get_history" => Params::ScriptHashGetHistory(convert(params)?),
|
||||||
|
137
src/status.rs
137
src/status.rs
@ -4,7 +4,7 @@ use bitcoin::{
|
|||||||
Amount, Block, BlockHash, OutPoint, SignedAmount, Transaction, Txid,
|
Amount, Block, BlockHash, OutPoint, SignedAmount, Transaction, Txid,
|
||||||
};
|
};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use serde::ser::{Serialize, Serializer};
|
use serde::ser::{Serialize, SerializeMap, Serializer};
|
||||||
|
|
||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
@ -48,12 +48,26 @@ impl TxEntry {
|
|||||||
// Confirmation height of a transaction or its mempool state:
|
// Confirmation height of a transaction or its mempool state:
|
||||||
// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-history
|
// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-history
|
||||||
// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-mempool
|
// https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-get-mempool
|
||||||
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
enum Height {
|
enum Height {
|
||||||
Confirmed { height: usize },
|
Confirmed { height: usize },
|
||||||
Unconfirmed { has_unconfirmed_inputs: bool },
|
Unconfirmed { has_unconfirmed_inputs: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Height {
|
impl Height {
|
||||||
|
fn from_blockhash(blockhash: BlockHash, chain: &Chain) -> Self {
|
||||||
|
let height = chain
|
||||||
|
.get_block_height(&blockhash)
|
||||||
|
.expect("missing block in chain");
|
||||||
|
Self::Confirmed { height }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unconfirmed(e: &crate::mempool::Entry) -> Self {
|
||||||
|
Self::Unconfirmed {
|
||||||
|
has_unconfirmed_inputs: e.has_unconfirmed_inputs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn as_i64(&self) -> i64 {
|
fn as_i64(&self) -> i64 {
|
||||||
match self {
|
match self {
|
||||||
Self::Confirmed { height } => i64::try_from(*height).unwrap(),
|
Self::Confirmed { height } => i64::try_from(*height).unwrap(),
|
||||||
@ -538,6 +552,127 @@ fn filter_block_txs<T: Send>(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct OutPointStatus {
|
||||||
|
outpoint: OutPoint,
|
||||||
|
funding: Option<Height>,
|
||||||
|
spending: Option<(Txid, Height)>,
|
||||||
|
tip: BlockHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for OutPointStatus {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let mut map = serializer.serialize_map(None)?;
|
||||||
|
if let Some(funding) = &self.funding {
|
||||||
|
map.serialize_entry("height", &funding)?;
|
||||||
|
}
|
||||||
|
if let Some((txid, height)) = &self.spending {
|
||||||
|
map.serialize_entry("spender_txhash", &txid)?;
|
||||||
|
map.serialize_entry("spender_height", &height)?;
|
||||||
|
}
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutPointStatus {
|
||||||
|
pub(crate) fn new(outpoint: OutPoint) -> Self {
|
||||||
|
Self {
|
||||||
|
outpoint,
|
||||||
|
funding: None,
|
||||||
|
spending: None,
|
||||||
|
tip: BlockHash::all_zeros(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sync(
|
||||||
|
&mut self,
|
||||||
|
index: &Index,
|
||||||
|
mempool: &Mempool,
|
||||||
|
daemon: &Daemon,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let funding = self.sync_funding(index, daemon, mempool)?;
|
||||||
|
let spending = self.sync_spending(index, daemon, mempool)?;
|
||||||
|
let same_status = (self.funding == funding) && (self.spending == spending);
|
||||||
|
self.funding = funding;
|
||||||
|
self.spending = spending;
|
||||||
|
self.tip = index.chain().tip();
|
||||||
|
Ok(!same_status)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return true iff current tip became unconfirmed
|
||||||
|
fn is_reorg(&self, chain: &Chain) -> bool {
|
||||||
|
chain.get_block_height(&self.tip).is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_funding(
|
||||||
|
&self,
|
||||||
|
index: &Index,
|
||||||
|
daemon: &Daemon,
|
||||||
|
mempool: &Mempool,
|
||||||
|
) -> Result<Option<Height>> {
|
||||||
|
let chain = index.chain();
|
||||||
|
if !self.is_reorg(chain) {
|
||||||
|
if let Some(Height::Confirmed { .. }) = &self.funding {
|
||||||
|
return Ok(self.funding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut confirmed = None;
|
||||||
|
daemon.for_blocks(
|
||||||
|
index.filter_by_txid(self.outpoint.txid),
|
||||||
|
|blockhash, block| {
|
||||||
|
if confirmed.is_none() {
|
||||||
|
for tx in block.txdata {
|
||||||
|
let txid = tx.txid();
|
||||||
|
let output_len = u32::try_from(tx.output.len()).unwrap();
|
||||||
|
if self.outpoint.txid == txid && self.outpoint.vout < output_len {
|
||||||
|
confirmed = Some(Height::from_blockhash(blockhash, chain));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
Ok(confirmed.or_else(|| mempool.get(&self.outpoint.txid).map(Height::unconfirmed)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync_spending(
|
||||||
|
&self,
|
||||||
|
index: &Index,
|
||||||
|
daemon: &Daemon,
|
||||||
|
mempool: &Mempool,
|
||||||
|
) -> Result<Option<(Txid, Height)>> {
|
||||||
|
let chain = index.chain();
|
||||||
|
if !self.is_reorg(chain) {
|
||||||
|
if let Some((_, Height::Confirmed { .. })) = &self.spending {
|
||||||
|
return Ok(self.spending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let spending_blockhashes = index.filter_by_spending(self.outpoint);
|
||||||
|
let mut confirmed = None;
|
||||||
|
daemon.for_blocks(spending_blockhashes, |blockhash, block| {
|
||||||
|
for tx in block.txdata {
|
||||||
|
for txi in &tx.input {
|
||||||
|
if txi.previous_output == self.outpoint {
|
||||||
|
// TODO: there should be only one spending input
|
||||||
|
assert!(confirmed.is_none(), "double spend of {}", self.outpoint);
|
||||||
|
confirmed = Some((tx.txid(), Height::from_blockhash(blockhash, chain)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok(confirmed.or_else(|| {
|
||||||
|
let entries = mempool.filter_by_spending(&self.outpoint);
|
||||||
|
assert!(entries.len() <= 1, "double spend of {}", self.outpoint);
|
||||||
|
entries
|
||||||
|
.first()
|
||||||
|
.map(|entry| (entry.txid, Height::unconfirmed(entry)))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::HistoryEntry;
|
use super::HistoryEntry;
|
||||||
|
@ -11,7 +11,7 @@ use crate::{
|
|||||||
mempool::{FeeHistogram, Mempool},
|
mempool::{FeeHistogram, Mempool},
|
||||||
metrics::Metrics,
|
metrics::Metrics,
|
||||||
signals::ExitFlag,
|
signals::ExitFlag,
|
||||||
status::{Balance, ScriptHashStatus, UnspentEntry},
|
status::{Balance, OutPointStatus, ScriptHashStatus, UnspentEntry},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Electrum protocol subscriptions' tracker
|
/// Electrum protocol subscriptions' tracker
|
||||||
@ -114,4 +114,12 @@ impl Tracker {
|
|||||||
})?;
|
})?;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update_outpoint_status(
|
||||||
|
&self,
|
||||||
|
status: &mut OutPointStatus,
|
||||||
|
daemon: &Daemon,
|
||||||
|
) -> Result<bool> {
|
||||||
|
status.sync(&self.index, &self.mempool, daemon)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user