2021-10-29 17:41:02 +11:00
|
|
|
/*!
|
|
|
|
This models a how a sync happens where you have a server that you send your script pubkeys to and it
|
|
|
|
returns associated transactions i.e. electrum.
|
|
|
|
*/
|
|
|
|
#![allow(dead_code)]
|
|
|
|
use crate::{
|
|
|
|
database::{BatchDatabase, BatchOperations, DatabaseUtils},
|
2021-11-02 17:16:03 +11:00
|
|
|
wallet::time::Instant,
|
2021-11-23 11:28:18 +11:00
|
|
|
BlockTime, Error, KeychainKind, LocalUtxo, TransactionDetails,
|
2021-10-29 17:41:02 +11:00
|
|
|
};
|
|
|
|
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
|
2021-11-02 17:16:03 +11:00
|
|
|
use log::*;
|
2021-11-05 12:34:17 +11:00
|
|
|
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
2021-10-29 17:41:02 +11:00
|
|
|
|
2021-11-02 17:22:24 +11:00
|
|
|
/// A request for on-chain information
|
2021-10-29 17:41:02 +11:00
|
|
|
pub enum Request<'a, D: BatchDatabase> {
|
|
|
|
/// A request for transactions related to script pubkeys.
|
|
|
|
Script(ScriptReq<'a, D>),
|
|
|
|
/// A request for confirmation times for some transactions.
|
|
|
|
Conftime(ConftimeReq<'a, D>),
|
|
|
|
/// A request for full transaction details of some transactions.
|
|
|
|
Tx(TxReq<'a, D>),
|
|
|
|
/// Requests are finished here's a batch database update to reflect data gathered.
|
|
|
|
Finish(D::Batch),
|
|
|
|
}
|
|
|
|
|
|
|
|
/// starts a sync
|
|
|
|
pub fn start<D: BatchDatabase>(db: &D, stop_gap: usize) -> Result<Request<'_, D>, Error> {
|
|
|
|
use rand::seq::SliceRandom;
|
|
|
|
let mut keychains = vec![KeychainKind::Internal, KeychainKind::External];
|
|
|
|
// shuffling improve privacy, the server doesn't know my first request is from my internal or external addresses
|
|
|
|
keychains.shuffle(&mut rand::thread_rng());
|
|
|
|
let keychain = keychains.pop().unwrap();
|
|
|
|
let scripts_needed = db
|
|
|
|
.iter_script_pubkeys(Some(keychain))?
|
|
|
|
.into_iter()
|
|
|
|
.collect();
|
2021-11-02 17:16:03 +11:00
|
|
|
let state = State::new(db);
|
2021-10-29 17:41:02 +11:00
|
|
|
|
|
|
|
Ok(Request::Script(ScriptReq {
|
|
|
|
state,
|
|
|
|
scripts_needed,
|
|
|
|
script_index: 0,
|
|
|
|
stop_gap,
|
|
|
|
keychain,
|
|
|
|
next_keychains: keychains,
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct ScriptReq<'a, D: BatchDatabase> {
|
|
|
|
state: State<'a, D>,
|
|
|
|
script_index: usize,
|
|
|
|
scripts_needed: VecDeque<Script>,
|
|
|
|
stop_gap: usize,
|
|
|
|
keychain: KeychainKind,
|
|
|
|
next_keychains: Vec<KeychainKind>,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The sync starts by returning script pubkeys we are interested in.
|
|
|
|
impl<'a, D: BatchDatabase> ScriptReq<'a, D> {
|
|
|
|
pub fn request(&self) -> impl Iterator<Item = &Script> + Clone {
|
|
|
|
self.scripts_needed.iter()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn satisfy(
|
|
|
|
mut self,
|
|
|
|
// we want to know the txids assoiciated with the script and their height
|
|
|
|
txids: Vec<Vec<(Txid, Option<u32>)>>,
|
|
|
|
) -> Result<Request<'a, D>, Error> {
|
2021-11-02 17:16:03 +11:00
|
|
|
for (txid_list, script) in txids.iter().zip(self.scripts_needed.iter()) {
|
|
|
|
debug!(
|
|
|
|
"found {} transactions for script pubkey {}",
|
|
|
|
txid_list.len(),
|
|
|
|
script
|
|
|
|
);
|
2021-10-29 17:41:02 +11:00
|
|
|
if !txid_list.is_empty() {
|
|
|
|
// the address is active
|
|
|
|
self.state
|
|
|
|
.last_active_index
|
|
|
|
.insert(self.keychain, self.script_index);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (txid, height) in txid_list {
|
|
|
|
// have we seen this txid already?
|
|
|
|
match self.state.db.get_tx(txid, true)? {
|
|
|
|
Some(mut details) => {
|
|
|
|
let old_height = details.confirmation_time.as_ref().map(|x| x.height);
|
|
|
|
match (old_height, height) {
|
|
|
|
(None, Some(_)) => {
|
|
|
|
// It looks like the tx has confirmed since we last saw it -- we
|
|
|
|
// need to know the confirmation time.
|
2021-11-05 12:34:17 +11:00
|
|
|
self.state.tx_missing_conftime.insert(*txid, details);
|
2021-10-29 17:41:02 +11:00
|
|
|
}
|
|
|
|
(Some(old_height), Some(new_height)) if old_height != *new_height => {
|
2021-11-05 12:44:36 +11:00
|
|
|
// The height of the tx has changed !? -- It's a reorg get the new confirmation time.
|
2021-11-05 12:34:17 +11:00
|
|
|
self.state.tx_missing_conftime.insert(*txid, details);
|
2021-10-29 17:41:02 +11:00
|
|
|
}
|
|
|
|
(Some(_), None) => {
|
2021-11-05 12:44:36 +11:00
|
|
|
// A re-org where the tx is not in the chain anymore.
|
2021-10-29 17:41:02 +11:00
|
|
|
details.confirmation_time = None;
|
2021-11-05 12:14:42 +11:00
|
|
|
self.state.finished_txs.push(details);
|
2021-10-29 17:41:02 +11:00
|
|
|
}
|
2021-11-05 12:14:42 +11:00
|
|
|
_ => self.state.finished_txs.push(details),
|
2021-10-29 17:41:02 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
None => {
|
|
|
|
// we've never seen it let's get the whole thing
|
2021-11-05 12:34:17 +11:00
|
|
|
self.state.tx_needed.insert(*txid);
|
2021-10-29 17:41:02 +11:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
self.script_index += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
for _ in txids {
|
|
|
|
self.scripts_needed.pop_front();
|
|
|
|
}
|
|
|
|
|
|
|
|
let last_active_index = self
|
|
|
|
.state
|
|
|
|
.last_active_index
|
|
|
|
.get(&self.keychain)
|
|
|
|
.map(|x| x + 1)
|
|
|
|
.unwrap_or(0); // so no addresses active maps to 0
|
|
|
|
|
|
|
|
Ok(
|
|
|
|
if self.script_index > last_active_index + self.stop_gap
|
|
|
|
|| self.scripts_needed.is_empty()
|
|
|
|
{
|
2021-11-02 17:16:03 +11:00
|
|
|
debug!(
|
|
|
|
"finished scanning for transactions for keychain {:?} at index {}",
|
|
|
|
self.keychain, last_active_index
|
|
|
|
);
|
2021-10-29 17:41:02 +11:00
|
|
|
// we're done here -- check if we need to do the next keychain
|
|
|
|
if let Some(keychain) = self.next_keychains.pop() {
|
|
|
|
self.keychain = keychain;
|
|
|
|
self.script_index = 0;
|
|
|
|
self.scripts_needed = self
|
|
|
|
.state
|
|
|
|
.db
|
|
|
|
.iter_script_pubkeys(Some(keychain))?
|
|
|
|
.into_iter()
|
|
|
|
.collect();
|
|
|
|
Request::Script(self)
|
|
|
|
} else {
|
2021-11-02 16:34:50 +11:00
|
|
|
Request::Tx(TxReq { state: self.state })
|
2021-10-29 17:41:02 +11:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Request::Script(self)
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Then we get full transactions
|
|
|
|
pub struct TxReq<'a, D> {
|
|
|
|
state: State<'a, D>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, D: BatchDatabase> TxReq<'a, D> {
|
|
|
|
pub fn request(&self) -> impl Iterator<Item = &Txid> + Clone {
|
|
|
|
self.state.tx_needed.iter()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn satisfy(
|
|
|
|
mut self,
|
2021-11-02 16:34:50 +11:00
|
|
|
tx_details: Vec<(Vec<Option<TxOut>>, Transaction)>,
|
2021-10-29 17:41:02 +11:00
|
|
|
) -> Result<Request<'a, D>, Error> {
|
|
|
|
let tx_details: Vec<TransactionDetails> = tx_details
|
|
|
|
.into_iter()
|
|
|
|
.zip(self.state.tx_needed.iter())
|
2021-11-05 13:30:41 +11:00
|
|
|
.map(|((vout, tx), txid)| {
|
2021-11-02 17:16:03 +11:00
|
|
|
debug!("found tx_details for {}", txid);
|
2021-10-29 17:41:02 +11:00
|
|
|
assert_eq!(tx.txid(), *txid);
|
|
|
|
let mut sent: u64 = 0;
|
|
|
|
let mut received: u64 = 0;
|
|
|
|
let mut inputs_sum: u64 = 0;
|
|
|
|
let mut outputs_sum: u64 = 0;
|
|
|
|
|
2022-01-21 21:43:05 +05:30
|
|
|
for (txout, (_input_index, input)) in
|
|
|
|
vout.into_iter().zip(tx.input.iter().enumerate())
|
|
|
|
{
|
2021-10-29 17:41:02 +11:00
|
|
|
let txout = match txout {
|
|
|
|
Some(txout) => txout,
|
|
|
|
None => {
|
|
|
|
// skip coinbase inputs
|
|
|
|
debug_assert!(
|
|
|
|
input.previous_output.is_null(),
|
|
|
|
"prevout should only be missing for coinbase"
|
|
|
|
);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
};
|
2022-01-21 21:43:05 +05:30
|
|
|
// Verify this input if requested via feature flag
|
|
|
|
#[cfg(feature = "verify")]
|
|
|
|
{
|
|
|
|
use crate::wallet::verify::VerifyError;
|
|
|
|
let serialized_tx = bitcoin::consensus::serialize(&tx);
|
|
|
|
bitcoinconsensus::verify(
|
|
|
|
txout.script_pubkey.to_bytes().as_ref(),
|
|
|
|
txout.value,
|
|
|
|
&serialized_tx,
|
|
|
|
_input_index,
|
|
|
|
)
|
|
|
|
.map_err(VerifyError::from)?;
|
|
|
|
}
|
2021-10-29 17:41:02 +11:00
|
|
|
inputs_sum += txout.value;
|
|
|
|
if self.state.db.is_mine(&txout.script_pubkey)? {
|
|
|
|
sent += txout.value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for out in &tx.output {
|
|
|
|
outputs_sum += out.value;
|
|
|
|
if self.state.db.is_mine(&out.script_pubkey)? {
|
|
|
|
received += out.value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// we need to saturating sub since we want coinbase txs to map to 0 fee and
|
|
|
|
// this subtraction will be negative for coinbase txs.
|
|
|
|
let fee = inputs_sum.saturating_sub(outputs_sum);
|
|
|
|
Result::<_, Error>::Ok(TransactionDetails {
|
|
|
|
txid: *txid,
|
|
|
|
transaction: Some(tx),
|
|
|
|
received,
|
|
|
|
sent,
|
2021-11-02 16:34:50 +11:00
|
|
|
// we're going to fill this in later
|
|
|
|
confirmation_time: None,
|
2021-10-29 17:41:02 +11:00
|
|
|
fee: Some(fee),
|
|
|
|
})
|
|
|
|
})
|
|
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
|
|
|
|
for tx_detail in tx_details {
|
2021-11-05 12:34:17 +11:00
|
|
|
self.state.tx_needed.remove(&tx_detail.txid);
|
2021-11-02 16:34:50 +11:00
|
|
|
self.state
|
|
|
|
.tx_missing_conftime
|
|
|
|
.insert(tx_detail.txid, tx_detail);
|
2021-10-29 17:41:02 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
if !self.state.tx_needed.is_empty() {
|
|
|
|
Ok(Request::Tx(self))
|
|
|
|
} else {
|
2021-11-02 16:34:50 +11:00
|
|
|
Ok(Request::Conftime(ConftimeReq { state: self.state }))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-02 17:16:03 +11:00
|
|
|
/// Final step is to get confirmation times
|
|
|
|
pub struct ConftimeReq<'a, D> {
|
|
|
|
state: State<'a, D>,
|
|
|
|
}
|
|
|
|
|
2021-11-02 16:34:50 +11:00
|
|
|
impl<'a, D: BatchDatabase> ConftimeReq<'a, D> {
|
|
|
|
pub fn request(&self) -> impl Iterator<Item = &Txid> + Clone {
|
2021-11-05 12:34:17 +11:00
|
|
|
self.state.tx_missing_conftime.keys()
|
2021-11-02 16:34:50 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn satisfy(
|
|
|
|
mut self,
|
2021-11-23 11:28:18 +11:00
|
|
|
confirmation_times: Vec<Option<BlockTime>>,
|
2021-11-02 16:34:50 +11:00
|
|
|
) -> Result<Request<'a, D>, Error> {
|
2021-11-05 12:34:17 +11:00
|
|
|
let conftime_needed = self
|
|
|
|
.request()
|
|
|
|
.cloned()
|
|
|
|
.take(confirmation_times.len())
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
for (confirmation_time, txid) in confirmation_times.into_iter().zip(conftime_needed.iter())
|
|
|
|
{
|
2021-11-02 17:16:03 +11:00
|
|
|
debug!("confirmation time for {} was {:?}", txid, confirmation_time);
|
2021-11-05 12:34:17 +11:00
|
|
|
if let Some(mut tx_details) = self.state.tx_missing_conftime.remove(txid) {
|
2021-11-02 16:34:50 +11:00
|
|
|
tx_details.confirmation_time = confirmation_time;
|
2021-11-05 12:14:42 +11:00
|
|
|
self.state.finished_txs.push(tx_details);
|
2021-11-02 16:34:50 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-05 12:34:17 +11:00
|
|
|
if self.state.tx_missing_conftime.is_empty() {
|
2021-11-02 16:34:50 +11:00
|
|
|
Ok(Request::Finish(self.state.into_db_update()?))
|
|
|
|
} else {
|
|
|
|
Ok(Request::Conftime(self))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct State<'a, D> {
|
|
|
|
db: &'a D,
|
|
|
|
last_active_index: HashMap<KeychainKind, usize>,
|
2021-11-05 12:14:42 +11:00
|
|
|
/// Transactions where we need to get the full details
|
2021-11-05 12:34:17 +11:00
|
|
|
tx_needed: BTreeSet<Txid>,
|
2021-11-05 12:14:42 +11:00
|
|
|
/// Transacitions that we know everything about
|
|
|
|
finished_txs: Vec<TransactionDetails>,
|
|
|
|
/// Transactions that discovered conftimes should be inserted into
|
2021-11-05 12:34:17 +11:00
|
|
|
tx_missing_conftime: BTreeMap<Txid, TransactionDetails>,
|
2021-11-05 12:14:42 +11:00
|
|
|
/// The start of the sync
|
2021-11-02 17:16:03 +11:00
|
|
|
start_time: Instant,
|
2021-11-02 16:34:50 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, D: BatchDatabase> State<'a, D> {
|
2021-11-02 17:16:03 +11:00
|
|
|
fn new(db: &'a D) -> Self {
|
|
|
|
State {
|
|
|
|
db,
|
|
|
|
last_active_index: HashMap::default(),
|
2021-11-05 12:14:42 +11:00
|
|
|
finished_txs: vec![],
|
2021-11-05 12:34:17 +11:00
|
|
|
tx_needed: BTreeSet::default(),
|
|
|
|
tx_missing_conftime: BTreeMap::default(),
|
2021-11-02 17:16:03 +11:00
|
|
|
start_time: Instant::new(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
fn into_db_update(self) -> Result<D::Batch, Error> {
|
2021-11-05 12:34:17 +11:00
|
|
|
debug_assert!(self.tx_needed.is_empty() && self.tx_missing_conftime.is_empty());
|
2021-11-02 16:34:50 +11:00
|
|
|
let existing_txs = self.db.iter_txs(false)?;
|
|
|
|
let existing_txids: HashSet<Txid> = existing_txs.iter().map(|tx| tx.txid).collect();
|
2021-11-05 12:14:42 +11:00
|
|
|
let finished_txs = make_txs_consistent(&self.finished_txs);
|
|
|
|
let observed_txids: HashSet<Txid> = finished_txs.iter().map(|tx| tx.txid).collect();
|
2021-11-02 16:34:50 +11:00
|
|
|
let txids_to_delete = existing_txids.difference(&observed_txids);
|
|
|
|
let mut batch = self.db.begin_batch();
|
|
|
|
|
|
|
|
// Delete old txs that no longer exist
|
|
|
|
for txid in txids_to_delete {
|
|
|
|
if let Some(raw_tx) = self.db.get_raw_tx(txid)? {
|
|
|
|
for i in 0..raw_tx.output.len() {
|
|
|
|
// Also delete any utxos from the txs that no longer exist.
|
|
|
|
let _ = batch.del_utxo(&OutPoint {
|
|
|
|
txid: *txid,
|
|
|
|
vout: i as u32,
|
|
|
|
})?;
|
2021-10-29 17:41:02 +11:00
|
|
|
}
|
2021-11-02 16:34:50 +11:00
|
|
|
} else {
|
|
|
|
unreachable!("we should always have the raw tx");
|
2021-10-29 17:41:02 +11:00
|
|
|
}
|
2021-11-02 16:34:50 +11:00
|
|
|
batch.del_tx(txid, true)?;
|
|
|
|
}
|
2021-10-29 17:41:02 +11:00
|
|
|
|
2022-03-09 16:15:34 +01:00
|
|
|
let mut spent_utxos = HashSet::new();
|
|
|
|
|
|
|
|
// track all the spent utxos
|
|
|
|
for finished_tx in &finished_txs {
|
|
|
|
let tx = finished_tx
|
|
|
|
.transaction
|
|
|
|
.as_ref()
|
|
|
|
.expect("transaction will always be present here");
|
|
|
|
for input in &tx.input {
|
|
|
|
spent_utxos.insert(&input.previous_output);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// set every utxo we observed, unless it's already spent
|
|
|
|
// we don't do this in the loop above as we want to know all the spent outputs before
|
|
|
|
// adding the non-spent to the batch in case there are new tranasactions
|
|
|
|
// that spend form each other.
|
2021-11-05 12:14:42 +11:00
|
|
|
for finished_tx in &finished_txs {
|
|
|
|
let tx = finished_tx
|
2021-11-02 16:34:50 +11:00
|
|
|
.transaction
|
|
|
|
.as_ref()
|
|
|
|
.expect("transaction will always be present here");
|
|
|
|
for (i, output) in tx.output.iter().enumerate() {
|
|
|
|
if let Some((keychain, _)) =
|
|
|
|
self.db.get_path_from_script_pubkey(&output.script_pubkey)?
|
|
|
|
{
|
|
|
|
// add utxos we own from the new transactions we've seen.
|
2022-03-09 16:15:34 +01:00
|
|
|
let outpoint = OutPoint {
|
|
|
|
txid: finished_tx.txid,
|
|
|
|
vout: i as u32,
|
|
|
|
};
|
|
|
|
|
2021-11-02 16:34:50 +11:00
|
|
|
batch.set_utxo(&LocalUtxo {
|
2022-03-09 16:15:34 +01:00
|
|
|
outpoint,
|
2021-11-02 16:34:50 +11:00
|
|
|
txout: output.clone(),
|
|
|
|
keychain,
|
2022-03-09 16:15:34 +01:00
|
|
|
// Is this UTXO in the spent_utxos set?
|
|
|
|
is_spent: spent_utxos.get(&outpoint).is_some(),
|
2021-11-02 16:34:50 +11:00
|
|
|
})?;
|
2021-10-29 17:41:02 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-09 16:15:34 +01:00
|
|
|
batch.set_tx(finished_tx)?;
|
2021-11-02 16:34:50 +11:00
|
|
|
}
|
2021-10-29 17:41:02 +11:00
|
|
|
|
2021-11-02 16:34:50 +11:00
|
|
|
for (keychain, last_active_index) in self.last_active_index {
|
|
|
|
batch.set_last_index(keychain, last_active_index as u32)?;
|
2021-10-29 17:41:02 +11:00
|
|
|
}
|
2021-11-02 16:34:50 +11:00
|
|
|
|
2021-11-02 17:16:03 +11:00
|
|
|
info!(
|
|
|
|
"finished setup, elapsed {:?}ms",
|
|
|
|
self.start_time.elapsed().as_millis()
|
|
|
|
);
|
2021-11-02 16:34:50 +11:00
|
|
|
Ok(batch)
|
2021-10-29 17:41:02 +11:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Remove conflicting transactions -- tie breaking them by fee.
|
|
|
|
fn make_txs_consistent(txs: &[TransactionDetails]) -> Vec<&TransactionDetails> {
|
|
|
|
let mut utxo_index: HashMap<OutPoint, &TransactionDetails> = HashMap::default();
|
|
|
|
for tx in txs {
|
|
|
|
for input in &tx.transaction.as_ref().unwrap().input {
|
|
|
|
utxo_index
|
|
|
|
.entry(input.previous_output)
|
|
|
|
.and_modify(|existing| match (tx.fee, existing.fee) {
|
|
|
|
(Some(fee), Some(existing_fee)) if fee > existing_fee => *existing = tx,
|
|
|
|
(Some(_), None) => *existing = tx,
|
|
|
|
_ => { /* leave it the same */ }
|
|
|
|
})
|
|
|
|
.or_insert(tx);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
utxo_index
|
|
|
|
.into_iter()
|
|
|
|
.map(|(_, tx)| (tx.txid, tx))
|
|
|
|
.collect::<HashMap<_, _>>()
|
|
|
|
.into_iter()
|
|
|
|
.map(|(_, tx)| tx)
|
|
|
|
.collect()
|
|
|
|
}
|