use crate::bitcoin::{
consensus::Decodable,
hashes::{sha256, Hash, HashEngine},
Amount, BlockHash, OutPoint, SignedAmount, Transaction, Txid,
};
use anyhow::Result;
use bindex::{IndexedChain, ScriptHash};
use serde::ser::{Serialize, Serializer};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::convert::TryFrom;
use crate::{mempool::Mempool, types::StatusHash};
struct TxEntry {
txid: Txid,
outputs: Vec<TxOutput>, spent: Vec<OutPoint>, }
struct TxOutput {
index: u32,
value: Amount,
}
impl TxEntry {
fn new(txid: Txid) -> Self {
Self {
txid,
outputs: Vec::new(),
spent: Vec::new(),
}
}
fn funding_outpoints(&self) -> impl Iterator<Item = OutPoint> + '_ {
make_outpoints(self.txid, &self.outputs)
}
}
enum Height {
Confirmed { height: usize },
Unconfirmed { has_unconfirmed_inputs: bool },
}
impl Height {
fn as_i64(&self) -> i64 {
match self {
Self::Confirmed { height } => i64::try_from(*height).unwrap(),
Self::Unconfirmed {
has_unconfirmed_inputs: true,
} => -1,
Self::Unconfirmed {
has_unconfirmed_inputs: false,
} => 0,
}
}
}
impl Serialize for Height {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_i64(self.as_i64())
}
}
impl std::fmt::Display for Height {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_i64().fmt(f)
}
}
#[derive(Serialize)]
pub(crate) struct HistoryEntry {
#[serde(rename = "tx_hash")]
txid: Txid,
height: Height,
#[serde(
skip_serializing_if = "Option::is_none",
with = "crate::bitcoin::amount::serde::as_sat::opt"
)]
fee: Option<Amount>,
}
impl HistoryEntry {
fn hash(&self, engine: &mut sha256::HashEngine) {
let s = format!("{}:{}:", self.txid, self.height);
engine.input(s.as_bytes());
}
fn confirmed(txid: Txid, height: usize) -> Self {
Self {
txid,
height: Height::Confirmed { height },
fee: None,
}
}
fn unconfirmed(txid: Txid, has_unconfirmed_inputs: bool, fee: Amount) -> Self {
Self {
txid,
height: Height::Unconfirmed {
has_unconfirmed_inputs,
},
fee: Some(fee),
}
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct BlockKey(usize, BlockHash);
impl From<bindex::Location<'_>> for BlockKey {
fn from(location: bindex::Location<'_>) -> Self {
Self(location.block_height(), location.block_hash())
}
}
pub struct ScriptHashStatus {
scripthash: ScriptHash, confirmed: BTreeMap<BlockKey, Vec<TxEntry>>, mempool: Vec<TxEntry>, history: Vec<HistoryEntry>, statushash: Option<StatusHash>, }
#[derive(Default, Eq, PartialEq, Serialize)]
pub(crate) struct Balance {
#[serde(with = "crate::bitcoin::amount::serde::as_sat", rename = "confirmed")]
confirmed_balance: Amount,
#[serde(with = "crate::bitcoin::amount::serde::as_sat", rename = "unconfirmed")]
mempool_delta: SignedAmount,
}
#[derive(Serialize)]
pub(crate) struct UnspentEntry {
height: usize, tx_hash: Txid,
tx_pos: u32,
#[serde(with = "crate::bitcoin::amount::serde::as_sat")]
value: Amount,
}
#[derive(Default)]
struct Unspent {
outpoints: HashMap<OutPoint, (Amount, usize)>,
balance: Balance,
}
impl Unspent {
fn build(status: &ScriptHashStatus) -> Self {
let mut unspent = Unspent::default();
status
.confirmed_height_entries()
.for_each(|(height, entries)| entries.iter().for_each(|e| unspent.insert(e, height)));
status.confirmed_entries().for_each(|e| unspent.remove(e));
unspent.balance.confirmed_balance = unspent.balance();
status.mempool.iter().for_each(|e| unspent.insert(e, 0)); status.mempool.iter().for_each(|e| unspent.remove(e));
let total_balance = unspent.balance().to_signed().unwrap();
let confirmed_balance = unspent.balance.confirmed_balance.to_signed().unwrap();
unspent.balance.mempool_delta = total_balance - confirmed_balance;
unspent
}
fn into_entries(self) -> Vec<UnspentEntry> {
self.outpoints
.into_iter()
.map(|(outpoint, (value, height))| UnspentEntry {
height,
tx_hash: outpoint.txid,
tx_pos: outpoint.vout,
value,
})
.collect()
}
fn balance(&self) -> Amount {
self.outpoints
.values()
.fold(Amount::default(), |acc, v| acc + v.0)
}
fn insert(&mut self, entry: &TxEntry, height: usize) {
for output in &entry.outputs {
let outpoint = OutPoint {
txid: entry.txid,
vout: output.index,
};
self.outpoints.insert(outpoint, (output.value, height));
}
}
fn remove(&mut self, entry: &TxEntry) {
for spent in &entry.spent {
self.outpoints.remove(spent);
}
}
}
impl ScriptHashStatus {
pub fn new(scripthash: ScriptHash) -> Self {
Self {
scripthash,
confirmed: BTreeMap::new(),
mempool: Vec::new(),
history: Vec::new(),
statushash: None,
}
}
fn confirmed_height_entries<'a>(&'a self) -> impl Iterator<Item = (usize, &'a [TxEntry])> + 'a {
self.confirmed
.iter()
.map(move |(blockid, entries)| (blockid.0, &entries[..]))
}
fn confirmed_entries<'a>(&'a self) -> impl Iterator<Item = &'a TxEntry> + 'a {
self.confirmed_height_entries()
.flat_map(|(_height, entries)| entries)
}
fn confirmed_outpoints(&self) -> HashSet<OutPoint> {
self.confirmed_entries()
.flat_map(TxEntry::funding_outpoints)
.collect()
}
pub(crate) fn get_unspent(&self) -> Vec<UnspentEntry> {
Unspent::build(self).into_entries()
}
pub(crate) fn get_balance(&self) -> Balance {
Unspent::build(self).balance
}
pub(crate) fn get_history(&self) -> &[HistoryEntry] {
&self.history
}
fn get_confirmed_history(&self) -> Vec<HistoryEntry> {
self.confirmed_height_entries()
.collect::<BTreeMap<usize, &[TxEntry]>>()
.into_iter()
.flat_map(|(height, entries)| {
entries
.iter()
.map(move |e| HistoryEntry::confirmed(e.txid, height))
})
.collect()
}
fn get_mempool_history(&self, mempool: &Mempool) -> Vec<HistoryEntry> {
let mut entries = self
.mempool
.iter()
.filter_map(|e| mempool.get(&e.txid))
.collect::<Vec<_>>();
entries.sort_by_key(|e| (e.has_unconfirmed_inputs, e.txid));
entries
.into_iter()
.map(|e| HistoryEntry::unconfirmed(e.txid, e.has_unconfirmed_inputs, e.fee))
.collect()
}
fn sync_confirmed(&mut self, index: &IndexedChain) -> Result<HashSet<OutPoint>> {
let headers = index.headers();
let mut latest_header = None;
while let Some(entry) = self.confirmed.last_entry() {
let BlockKey(height, hash) = entry.key();
match headers.get_header(*hash, *height) {
Ok(header) => {
latest_header = Some(header);
break;
}
Err(err) => {
warn!("drop reorged block: {}", err);
entry.remove();
continue;
}
}
}
let mut outpoints = self.confirmed_outpoints();
for location in index.locations_by_scripthash(&self.scripthash, latest_header)? {
let tx_bytes = index.get_tx_bytes(&location)?;
let tx = Transaction::consensus_decode_from_finite_reader(&mut &tx_bytes[..])?;
let spent = filter_inputs(&tx, &outpoints);
let outputs = filter_outputs(&tx, self.scripthash);
if spent.is_empty() && outputs.is_empty() {
continue;
}
let mut tx_entry = TxEntry::new(tx.compute_txid());
tx_entry.spent = spent;
tx_entry.outputs = outputs;
outpoints.extend(tx_entry.funding_outpoints());
self.confirmed
.entry(BlockKey::from(location))
.or_default()
.push(tx_entry);
}
Ok(outpoints)
}
fn sync_mempool(&self, mempool: &Mempool, mut outpoints: HashSet<OutPoint>) -> Vec<TxEntry> {
let mut result = HashMap::<Txid, TxEntry>::new();
for entry in mempool.filter_by_funding(&self.scripthash) {
let funding_outputs = filter_outputs(&entry.tx, self.scripthash);
assert!(!funding_outputs.is_empty());
outpoints.extend(make_outpoints(entry.txid, &funding_outputs));
result
.entry(entry.txid) .or_insert_with(|| TxEntry::new(entry.txid))
.outputs = funding_outputs;
}
for entry in outpoints
.iter()
.flat_map(|outpoint| mempool.filter_by_spending(outpoint))
{
let spent_outpoints = filter_inputs(&entry.tx, &outpoints);
assert!(!spent_outpoints.is_empty());
result
.entry(entry.txid) .or_insert_with(|| TxEntry::new(entry.txid))
.spent = spent_outpoints;
}
result.into_values().collect()
}
pub(crate) fn sync(&mut self, index: &IndexedChain, mempool: &Mempool) -> Result<()> {
let outpoints = self.sync_confirmed(index)?;
if !self.confirmed.is_empty() {
debug!(
"{} transactions from {} blocks",
self.confirmed.values().map(Vec::len).sum::<usize>(),
self.confirmed.len()
);
}
self.mempool = self.sync_mempool(mempool, outpoints);
if !self.mempool.is_empty() {
debug!("{} mempool transactions", self.mempool.len());
}
self.history.clear();
self.history.extend(self.get_confirmed_history());
self.history.extend(self.get_mempool_history(mempool));
self.statushash = compute_status_hash(&self.history);
Ok(())
}
pub fn statushash(&self) -> Option<StatusHash> {
self.statushash
}
}
fn make_outpoints(txid: Txid, outputs: &[TxOutput]) -> impl Iterator<Item = OutPoint> + '_ {
outputs
.iter()
.map(move |out| OutPoint::new(txid, out.index))
}
fn filter_outputs(tx: &Transaction, scripthash: ScriptHash) -> Vec<TxOutput> {
let outputs = tx.output.iter().zip(0u32..);
outputs
.filter(|&(txo, _vout)| ScriptHash::new(&txo.script_pubkey) == scripthash)
.map(|(txo, vout)| TxOutput {
index: vout,
value: txo.value,
})
.collect()
}
fn filter_inputs(tx: &Transaction, outpoints: &HashSet<OutPoint>) -> Vec<OutPoint> {
let inputs = tx.input.iter();
inputs
.filter(|&txi| outpoints.contains(&txi.previous_output))
.map(|txi| txi.previous_output)
.collect()
}
fn compute_status_hash(history: &[HistoryEntry]) -> Option<StatusHash> {
if history.is_empty() {
return None;
}
let mut engine = StatusHash::engine();
for entry in history {
entry.hash(&mut engine);
}
Some(StatusHash::from_engine(engine))
}
#[cfg(test)]
mod tests {
use super::HistoryEntry;
use crate::bitcoin::Amount;
use serde_json::json;
#[test]
fn test_txinfo_json() {
let txid = "5b75086dafeede555fc8f9a810d8b10df57c46f9f176ccc3dd8d2fa20edd685b"
.parse()
.unwrap();
assert_eq!(
json!(HistoryEntry::confirmed(txid, 123456)),
json!({"tx_hash": "5b75086dafeede555fc8f9a810d8b10df57c46f9f176ccc3dd8d2fa20edd685b", "height": 123456})
);
assert_eq!(
json!(HistoryEntry::unconfirmed(txid, true, Amount::from_sat(123))),
json!({"tx_hash": "5b75086dafeede555fc8f9a810d8b10df57c46f9f176ccc3dd8d2fa20edd685b", "height": -1, "fee": 123})
);
assert_eq!(
json!(HistoryEntry::unconfirmed(
txid,
false,
Amount::from_sat(123)
)),
json!({"tx_hash": "5b75086dafeede555fc8f9a810d8b10df57c46f9f176ccc3dd8d2fa20edd685b", "height": 0, "fee": 123})
);
}
}