From ef0145fb9226197b9ca15c80403cdee20d8b179f Mon Sep 17 00:00:00 2001 From: Nikita Chashchinskii Date: Mon, 12 Dec 2022 09:43:39 +0400 Subject: [PATCH 1/2] Don't export attempt_bundle_broadcast --- src/drive/mod.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/drive/mod.rs b/src/drive/mod.rs index 5deb112..79d1118 100644 --- a/src/drive/mod.rs +++ b/src/drive/mod.rs @@ -93,15 +93,16 @@ impl Drivechain { prev_main_block_hash: &BlockHash, amount: Amount, ) -> Result<(), Error> { + self.attempt_bundle_broadcast()?; trace!( "attempting to create a bmm request for block with hash = {} and with bribe = {}", side_block_hash, amount ); // Create a BMM request. - let txid = self - .client - .send_bmm_request(side_block_hash, prev_main_block_hash, 0, amount)?; + let txid = + self.client + .send_bmm_request(side_block_hash, prev_main_block_hash, 0, amount)?; let bmm_request = BMMRequest { txid, prev_main_block_hash: *prev_main_block_hash, @@ -264,9 +265,7 @@ impl Drivechain { } // TODO: Raise alarm if bundle hash being voted on is wrong. - // FIXME: Make this method private and call it in `connect_block`. - /// This must be called after every block connection. - pub fn attempt_bundle_broadcast(&mut self) -> Result<(), Error> { + fn attempt_bundle_broadcast(&mut self) -> Result<(), Error> { trace!("attempting to create and broadcast a new bundle"); self.update_bundles()?; // Wait for some time after a failed bundle to give people an @@ -305,7 +304,7 @@ impl Drivechain { } }; let status = self.get_bundle_status(&bundle.txid())?; - info!("bundle {} created it is {}", bundle.txid(), status,); + info!("bundle {} created, it is {}", bundle.txid(), status,); trace!("bundle = {:?}", bundle); // We broadcast a bundle only if it was not seen before, meaning it is // neither failed nor spent. From 26cb4af13219dd15f274b01516ddde7932d9acb5 Mon Sep 17 00:00:00 2001 From: Nikita Chashchinskii Date: Sat, 17 Dec 2022 13:56:06 +0400 Subject: [PATCH 2/2] Add a property test for _update_deposits --- Cargo.toml | 7 + src/drive/db.rs | 453 ++++++++++++++++++++++++++++++++++++++----- src/drive/deposit.rs | 42 +++- src/drive/mod.rs | 150 +++++++------- 4 files changed, 518 insertions(+), 134 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5e701b2..3206eb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,10 @@ thiserror = "1.0.31" log = "0.4" chrono = "0.4" env_logger = "0.9" +rusqlite = { version = "0.28.0", features = ["bundled"] } + +[dev-dependencies] +rand = "0.8.5" +fake = { version = "2.5.0", features = ["derive"] } +quickcheck = "1.0.3" +quickcheck_macros = "1.0.0" diff --git a/src/drive/db.rs b/src/drive/db.rs index 4a6ee0d..f2f8f7d 100644 --- a/src/drive/db.rs +++ b/src/drive/db.rs @@ -1,32 +1,28 @@ -use super::deposit::{Deposit, MainDeposit}; +// FIXME: Write down how this all works in plain english. +// FIXME: This file is ill structured and too long. Restructure it and document +// it thoroughly. +// FIXME: Add unit tests for every function. +// FIXME: Use SQLite instead of sled. +use super::deposit::{Deposit, DepositUpdate, MainDeposit}; use super::withdrawal::Withdrawal; -use bitcoin::blockdata::transaction::OutPoint; use bitcoin::blockdata::{ opcodes, script, - transaction::{TxIn, TxOut}, + transaction::{OutPoint, TxIn, TxOut}, }; -use bitcoin::hash_types::{ScriptHash, Txid}; +use bitcoin::hash_types::{BlockHash, ScriptHash, Txid}; use bitcoin::hashes::Hash; use bitcoin::util::amount::Amount; use byteorder::{BigEndian, ByteOrder}; use log::{error, trace}; +use rusqlite::{Connection, OptionalExtension, Result}; use sled::transaction::{abort, TransactionError}; use sled::Transactional; use std::collections::{HashMap, HashSet}; -const DEPOSITS: &[u8] = b"deposits"; -const DEPOSIT_BALANCES: &[u8] = b"deposit_balances"; -const UNBALANCED_DEPOSITS: &[u8] = b"unbalanced_deposits"; - -const OUTPOINT_TO_WITHDRAWAL: &[u8] = b"outpoint_to_withdrawal"; -const SPENT_OUTPOINTS: &[u8] = b"spent_outpoints"; -const UNSPENT_OUTPOINTS: &[u8] = b"unspent_outpoints"; - -const BUNDLE_HASH_TO_INPUTS: &[u8] = b"bundle_hash_to_inputs"; -const FAILED_BUNDLE_HASHES: &[u8] = b"failed_bundle_hashes"; -const SPENT_BUNDLE_HASHES: &[u8] = b"spent_bundle_hashes"; - -const VALUES: &[u8] = b"values"; +#[cfg(test)] +use fake::{Dummy, Fake, Faker}; +#[cfg(test)] +use quickcheck_macros::quickcheck; // Current sidechain block height. const SIDE_BLOCK_HEIGHT: &[u8] = b"side_block_height"; @@ -35,43 +31,101 @@ const LAST_FAILED_BUNDLE_HEIGHT: &[u8] = b"last_failed_bundle_height"; // Mainchain block height of the last known bmm commitment. const LAST_BMM_COMMITMENT_MAIN_BLOCK_HEIGHT: &[u8] = b"last_bmm_commitment_main_block_height"; +#[derive(Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct Outpoint(Vec); + +impl Outpoint { + fn bytes(&self) -> &[u8] { + self.0.as_slice() + } + fn hex(&self) -> String { + hex::encode(self.bytes()) + } +} +#[derive(Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct Address(String); + +// TODO: Implement proper transactions support in typed_sled or write another +// typed wrapper over sled. pub struct DB { + conn: Connection, db: sled::Db, + // Index -> Deposit deposits: sled::Tree, + // Address -> (u64, u64) deposit_balances: sled::Tree, + // Address -> () unbalanced_deposits: sled::Tree, + // Outpoint -> Withdrawal outpoint_to_withdrawal: sled::Tree, + // Outpoint -> () spent_outpoints: sled::Tree, + // Outpoint -> () unspent_outpoints: sled::Tree, + // Txid -> Vec bundle_hash_to_inputs: sled::Tree, // Failed and spent bundle hashes that we have already seen. + // Txid -> () failed_bundle_hashes: sled::Tree, + // Txid -> () spent_bundle_hashes: sled::Tree, // Store for values like: // block_height // last_failed_bundle_height // last_valid_bmm_main_block_height + // String -> u64 values: sled::Tree, } impl DB { pub fn new + std::fmt::Display>(path: P) -> Result { - trace!("creating drivechain object with path = {}", path); + trace!("creating drivechain database with path = {}", path); + let conn = Connection::open_in_memory()?; + conn.execute( + "CREATE TABLE deposit ( + id INTEGER PRIMARY KEY, + blockhash BLOB NOT NULL, + txid BLOB NOT NULL, + vout INTEGER NOT NULL, + amount INTEGER NOT NULL, + strdest TEXT NOT NULL + )", + (), // empty list of parameters. + )?; + conn.execute( + "CREATE TABLE deposit_balance ( + address TEXT PRIMARY KEY, + delta INTEGER NOT NULL + )", + (), // empty list of parameters. + )?; + conn.execute( + "CREATE TABLE withdrawal ( + outpoint BLOB PRIMARY KEY, + fee INTEGER NOT NULL, + height INTEGER NOT NULL, + dest BLOB NOT NULL, + amount INTEGER NOT NULL, + bundle BLOB + )", + (), // empty list of parameters. + )?; let db = sled::open(path)?; - let deposits = db.open_tree(DEPOSITS)?; - let deposit_balances = db.open_tree(DEPOSIT_BALANCES)?; - let unbalanced_deposits = db.open_tree(UNBALANCED_DEPOSITS)?; - let outpoint_to_withdrawal = db.open_tree(OUTPOINT_TO_WITHDRAWAL)?; - let spent_outpoints = db.open_tree(SPENT_OUTPOINTS)?; - let unspent_outpoints = db.open_tree(UNSPENT_OUTPOINTS)?; - let bundle_hash_to_inputs = db.open_tree(BUNDLE_HASH_TO_INPUTS)?; - let failed_bundle_hashes = db.open_tree(FAILED_BUNDLE_HASHES)?; - let spent_bundle_hashes = db.open_tree(SPENT_BUNDLE_HASHES)?; - let values = db.open_tree(VALUES)?; - trace!("drivechain object successfuly created"); + let deposits = db.open_tree("deposits")?; + let deposit_balances = db.open_tree("deposit_balances")?; + let unbalanced_deposits = db.open_tree("unbalanced_deposits")?; + let outpoint_to_withdrawal = db.open_tree("outpoint_to_withdrawal")?; + let spent_outpoints = db.open_tree("spent_outpoints")?; + let unspent_outpoints = db.open_tree("unspent_outpoints")?; + let bundle_hash_to_inputs = db.open_tree("bundle_hash_to_inputs")?; + let failed_bundle_hashes = db.open_tree("failed_bundle_hashes")?; + let spent_bundle_hashes = db.open_tree("spent_bundle_hashes")?; + let values = db.open_tree("values")?; + trace!("drivechain database successfuly created"); Ok(DB { + conn, db, deposits, deposit_balances, @@ -86,8 +140,19 @@ impl DB { }) } - pub fn flush(&mut self) -> Result { - self.db.flush().map_err(|err| err.into()) + pub fn _get_deposit_outputs(&self) -> Result, Error> { + let mut deposits = self + .conn + .prepare("SELECT address, delta FROM deposit_balance WHERE delta > 0")?; + let deposits = deposits + .query_map([], |row| { + let address: String = row.get(0)?; + let amount: u64 = row.get(1)?; + Ok((address, amount)) + })? + .map(|deposit| deposit.unwrap()) + .collect(); + Ok(deposits) } pub fn get_deposit_outputs(&self) -> Result, Error> { @@ -118,8 +183,9 @@ impl DB { pub fn connect_block( &mut self, deposits: &[Deposit], - withdrawals: &HashMap, Withdrawal>, - refunds: &HashMap, u64>, + withdrawals: &HashMap, + refunds: &HashMap, + // FIXME: Get rid of this flag. just_check: bool, ) -> Result<(), Error> { ( @@ -215,7 +281,7 @@ impl DB { let height = height + 1; values.insert(SIDE_BLOCK_HEIGHT, &height.to_be_bytes())?; for (outpoint, withdrawal) in withdrawals.iter() { - let outpoint = outpoint.as_slice(); + let outpoint = outpoint.bytes(); let withdrawal = Withdrawal { height, ..*withdrawal @@ -232,7 +298,7 @@ impl DB { // checking spent bundle validity. trace!("connecting {} refunds", refunds.len()); for (outpoint, amount) in refunds.iter() { - let outpoint = outpoint.as_slice(); + let outpoint = outpoint.bytes(); match outpoint_to_withdrawal.get(outpoint)? { Some(withdrawal) => { // It is unnecessary and inconvenient to check @@ -303,8 +369,9 @@ impl DB { pub fn disconnect_block( &mut self, deposits: &[Deposit], - withdrawals: &[Vec], - refunds: &[Vec], + withdrawals: &[Outpoint], + refunds: &[Outpoint], + // FIXME: Get rid of this flag. just_check: bool, ) -> Result<(), Error> { trace!( @@ -386,7 +453,7 @@ impl DB { let height = height - 1; values.insert(SIDE_BLOCK_HEIGHT, &height.to_be_bytes())?; for outpoint in withdrawals.iter() { - let outpoint = outpoint.as_slice(); + let outpoint = outpoint.bytes(); outpoint_to_withdrawal.remove(outpoint)?; unspent_outpoints.remove(outpoint)?; spent_outpoints.remove(outpoint)?; @@ -395,7 +462,7 @@ impl DB { trace!("disconnecting {} refunds", refunds.len()); for outpoint in refunds.iter() { - let outpoint = outpoint.as_slice(); + let outpoint = outpoint.bytes(); if outpoint_to_withdrawal.get(outpoint)?.is_none() { continue; } @@ -434,6 +501,93 @@ impl DB { .map(|height| BigEndian::read_u64(&height) as usize)) } + pub fn _update_deposits(&mut self, deposits: &[DepositUpdate]) -> Result<(), Error> { + let tx = self.conn.transaction()?; + trace!("updating deposits db with {} new deposits", deposits.len()); + // Get the last deposit stored in the db. + let last_deposit = DB::_get_last_deposit(&tx)?; + // Find the deposit that spends it. + let deposits: Vec<&DepositUpdate> = deposits + .iter() + .skip_while(|deposit| match &last_deposit { + Some(other) => !deposit.spends(other), + // If there are no deposits in db we don't skip any + // deposits. + None => false, + }) + .collect(); + + // Apply all new deposits to deposit_balance table. + let mut balances = HashMap::::new(); + let mut prev_amount = last_deposit + .map(|deposit| deposit.amount) + .unwrap_or(Amount::ZERO); + for deposit in deposits { + if deposit.amount < prev_amount { + continue; + } + trace!( + "added {} to {}", + deposit.amount - prev_amount, + deposit.strdest + ); + let balance = balances + .entry(deposit.strdest.clone()) + .or_insert(Amount::ZERO); + *balance += deposit.amount - prev_amount; + prev_amount = deposit.amount; + } + for (address, delta) in balances { + tx.execute( + "INSERT INTO deposit_balance (address, delta) + VALUES (?1, ?2) + ON CONFLICT (address) DO + UPDATE SET delta=delta+?2", + (address, delta.to_sat()), + )?; + } + tx.commit()?; + // Set new last_deposit. + Ok(()) + } + + fn _push_deposit(tx: &rusqlite::Transaction, deposit: &DepositUpdate) -> Result<(), Error> { + tx.execute("DELETE FROM last_ctip", ())?; + tx.execute( + "INSERT INTO deposit (blockhash, txid, vout, amount, strdest) VALUES (?, ?, ?, ?, ?)", + ( + deposit.blockhash.into_inner(), + deposit.ctip.txid.into_inner(), + deposit.ctip.vout, + deposit.amount.to_sat(), + deposit.strdest.clone(), + ), + )?; + Ok(()) + } + + fn _get_last_deposit(tx: &rusqlite::Transaction) -> Result, Error> { + let mut deposit = tx.prepare( + "SELECT id, blockhash, txid, vout, amount, strdest FROM deposit ORDER BY id", + )?; + let deposit = deposit + .query_row([], |row| { + let deposit = DepositUpdate { + index: row.get(0)?, + blockhash: BlockHash::from_inner(row.get(1)?), + ctip: OutPoint { + txid: Txid::from_inner(row.get(2)?), + vout: row.get(3)?, + }, + amount: Amount::from_sat(row.get(4)?), + strdest: row.get(5)?, + }; + Ok(deposit) + }) + .optional()?; + Ok(deposit) + } + pub fn update_deposits(&self, deposits: &[MainDeposit]) -> Result<(), Error> { trace!("updating deposits db with {} new deposits", deposits.len()); // New deposits are sorted in CTIP order. @@ -601,12 +755,14 @@ impl DB { .transaction( |(outpoint_to_withdrawal, unspent_outpoints, spent_outpoints)| { for input in inputs.iter() { - let withdrawal = outpoint_to_withdrawal.get(input)?; + // FIXME: Is this unwrap ok? + let withdrawal = + outpoint_to_withdrawal.get(bincode::serialize(input).unwrap())?; if withdrawal.is_none() { - return abort(Error::NoWithdrawalInDb(hex::encode(&input))); + return abort(Error::NoWithdrawalInDb(input.hex())); } - unspent_outpoints.remove(input.as_slice())?; - spent_outpoints.insert(input.as_slice(), &[])?; + unspent_outpoints.remove(input.bytes())?; + spent_outpoints.insert(input.bytes(), &[])?; } Ok(()) }, @@ -626,8 +782,8 @@ impl DB { |(spent_bundle_hashes, unspent_outpoints, spent_outpoints)| { spent_bundle_hashes.insert(txid.as_inner(), &[])?; for input in inputs.iter() { - unspent_outpoints.remove(input.as_slice())?; - spent_outpoints.insert(input.as_slice(), &[])?; + unspent_outpoints.remove(input.bytes())?; + spent_outpoints.insert(input.bytes(), &[])?; } Ok(()) }, @@ -648,8 +804,8 @@ impl DB { |(failed_bundle_hashes, unspent_outpoints, spent_outpoints, values)| { failed_bundle_hashes.insert(txid.as_inner(), &[])?; for input in inputs.iter() { - spent_outpoints.remove(input.as_slice())?; - unspent_outpoints.insert(input.as_slice(), &[])?; + spent_outpoints.remove(input.bytes())?; + unspent_outpoints.insert(input.bytes(), &[])?; } let last_failed_bundle_height = match values.get(SIDE_BLOCK_HEIGHT)? { Some(height) => height, @@ -702,16 +858,16 @@ impl DB { .collect() } - pub fn get_inputs(&mut self, txid: &Txid) -> Result>, Error> { + pub fn get_inputs(&mut self, txid: &Txid) -> Result, Error> { let hash = txid.into_inner(); let inputs = match self.bundle_hash_to_inputs.get(hash)? { Some(inputs) => inputs, None => return Ok(vec![]), }; - bincode::deserialize::>>(&inputs).map_err(|err| err.into()) + bincode::deserialize::>(&inputs).map_err(|err| err.into()) } - pub fn get_unspent_withdrawals(&self) -> Result, Withdrawal>, Error> { + pub fn get_unspent_withdrawals(&self) -> Result, Error> { self.unspent_outpoints .iter() .map(|item| { @@ -721,7 +877,7 @@ impl DB { None => return Err(Error::NoWithdrawalInDb(hex::encode(&outpoint))), }; let withdrawal = bincode::deserialize::(&withdrawal)?; - Ok((outpoint.to_vec(), withdrawal)) + Ok((Outpoint(outpoint.to_vec()), withdrawal)) }) .collect() } @@ -874,10 +1030,18 @@ impl DB { trace!("bundle was created successfuly"); Ok(Some(bundle)) } + + pub fn flush(&mut self) -> Result { + self.db.flush().map_err(|err| err.into()) + } } #[derive(thiserror::Error, Debug)] pub enum Error { + #[error("there are more than one ctip in last_ctip table")] + MultipleCtips, + #[error("rusqlite error")] + Rusqlite(#[from] rusqlite::Error), #[error("sled error")] Sled(#[from] sled::Error), #[error("bincode error")] @@ -940,3 +1104,190 @@ pub enum DisconnectError { #[error("cannot disconnect genesis block")] Genesis, } + +#[cfg(test)] +mod tests { + use super::*; + use rand::{prelude::SliceRandom, Rng, SeedableRng}; + + // Split a number into a vector of random terms. + struct SplitTerms(u64); + impl Dummy for Vec { + fn dummy_with_rng(number: &SplitTerms, rng: &mut R) -> Vec { + let SplitTerms(mut number) = *number; + let mut terms = vec![]; + while number > 0 { + let term = rng.gen_range(1..=number); + number -= term; + terms.push(term); + } + terms + } + } + + #[quickcheck] + fn terms_sum_to_number(number: u64) -> bool { + let terms = SplitTerms(number).fake::>(); + terms.iter().sum::() == number + } + + #[derive(Debug, Clone)] + struct Balances(HashMap); + + impl quickcheck::Arbitrary for Balances { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + g.size().fake::() + } + } + + impl Dummy for Balances { + // Generates a random Address -> Amount balances HashMap. + fn dummy_with_rng(size: &usize, rng: &mut R) -> Balances { + const ADDRESS_LENGTH: usize = 10; + let max_amount: u64 = Amount::MAX_MONEY.to_sat() / (*size as u64); + let balances: HashMap = (0..*size) + .map(|_| { + let address = ADDRESS_LENGTH.fake::(); + let balance = (0..max_amount).fake::(); + (address, balance) + }) + .collect(); + Balances(balances) + } + } + + impl Dummy for HashMap> { + // Split balances into vectors of random terms. + fn dummy_with_rng( + balances: &Balances, + rng: &mut R, + ) -> HashMap> { + let Balances(balances) = balances; + let balances = (*balances).clone(); + balances + .into_iter() + .map(|(address, balance)| (address, SplitTerms(balance).fake())) + .collect() + } + } + + #[quickcheck] + fn balance_terms_sum_to_balances(balances: HashMap) -> bool { + let split_balances = Balances(balances.clone()).fake::>>(); + let summed_balances = split_balances + .into_iter() + .map(|(address, terms)| (address, terms.iter().sum::())) + .collect(); + balances == summed_balances + } + + impl Dummy for Vec<(String, u64)> { + // Convert balances into a shuffled sequence of (address, delta) pairs. + fn dummy_with_rng(balances: &Balances, rng: &mut R) -> Vec<(String, u64)> { + let balances_split_into_terms: HashMap> = balances.fake(); + let mut address_delta_pairs: Vec<(String, u64)> = balances_split_into_terms + .into_iter() + .map(|(address, deltas)| std::iter::repeat(address).zip(deltas.into_iter())) + .flatten() + .collect(); + address_delta_pairs.shuffle(rng); + address_delta_pairs + } + } + + fn convert_to_totals(address_delta_pairs: Vec<(String, u64)>) -> Vec<(String, u64)> { + let mut address_total_pairs = vec![]; + let mut total = 0; + for (address, delta) in address_delta_pairs { + total += delta; + address_total_pairs.push((address, total)); + } + address_total_pairs + } + + impl Dummy for Vec { + // Convert balances into a shuffled sequence of (address, delta) pairs. + fn dummy_with_rng(balances: &Balances, rng: &mut R) -> Vec { + let address_delta_pairs = balances.fake::>(); + let address_total_pairs = convert_to_totals(address_delta_pairs); + let deposit_updates: Vec = address_total_pairs + .iter() + .enumerate() + .map(|(index, (address, total))| { + const MAX_VOUT: u32 = 100; + let blockhash: [u8; 32] = Faker.fake(); + let blockhash = BlockHash::from_inner(blockhash); + let txid: [u8; 32] = Faker.fake(); + let txid = Txid::from_inner(txid); + let vout: u32 = (0..MAX_VOUT).fake(); + let ctip = OutPoint { txid, vout }; + DepositUpdate { + index, + blockhash, + ctip, + strdest: address.clone(), + amount: Amount::from_sat(*total), + } + }) + .collect(); + deposit_updates + } + } + + #[derive(Clone, Debug)] + pub struct DepositUpdates(pub Vec); + + pub struct DepositsFaker { + num_deposits: usize, + max_vout: usize, + balances: HashMap, + } + + // impl Dummy for DepositUpdates { + // fn dummy_with_rng(config: &DepositsFaker, rng: &mut R) -> DepositUpdates {} + // } + + impl Dummy for DepositUpdates { + fn dummy_with_rng(_: &Faker, rng: &mut R) -> DepositUpdates { + const NUM_ADDRESSES: usize = 10; + const NUM_DEPOSITS: usize = 10000; + const MAX_VOUT: u32 = 100; + let max_amount: u64 = Amount::MAX_MONEY.to_sat() / (NUM_DEPOSITS as u64); + let mut amount = Amount::ZERO; + + let addresses: Vec = + (0..NUM_ADDRESSES).map(|_| Faker.fake::()).collect(); + let deposit_updates = (0..NUM_DEPOSITS) + .map(|index| { + let blockhash: [u8; 32] = Faker.fake(); + let blockhash = BlockHash::from_inner(blockhash); + let txid: [u8; 32] = Faker.fake(); + let txid = Txid::from_inner(txid); + let vout: u32 = (0..MAX_VOUT).fake(); + let delta: u64 = (0..max_amount).fake(); + let delta = Amount::from_sat(delta); + amount += delta; + let strdest: String = addresses.choose(rng).unwrap().clone(); + + DepositUpdate { + index, + blockhash, + ctip: OutPoint { txid, vout }, + amount, + strdest, + } + }) + .collect(); + DepositUpdates(deposit_updates) + } + } + + #[quickcheck] + fn deposits_work(balances: Balances) -> bool { + let mut db = DB::new("/tmp/drivechain").unwrap(); + let deposit_updates = balances.fake::>(); + db._update_deposits(&deposit_updates).unwrap(); + let outputs = db._get_deposit_outputs().unwrap(); + balances.0 == outputs + } +} diff --git a/src/drive/deposit.rs b/src/drive/deposit.rs index 5cdf720..af58979 100644 --- a/src/drive/deposit.rs +++ b/src/drive/deposit.rs @@ -1,17 +1,31 @@ use bitcoin::blockdata::transaction::{OutPoint, Transaction}; -use bitcoin::hash_types::{BlockHash}; +use bitcoin::hash_types::{BlockHash, Txid}; use bitcoin::hashes::Hash; use bitcoin::util::amount::Amount; -use bitcoin::util::psbt::serialize::{Deserialize, Serialize}; use serde::de::Error; +#[derive(Debug, Clone)] +pub struct DepositUpdate { + pub index: usize, + pub blockhash: BlockHash, + pub ctip: OutPoint, + pub amount: Amount, + pub strdest: String, +} + +impl DepositUpdate { + pub fn spends(&self, other: &DepositUpdate) -> bool { + return self.index > other.index && self.index - other.index == 1; + } +} + #[derive(Debug, Clone)] pub struct MainDeposit { pub blockhash: BlockHash, - pub ntx: usize, + pub ntx: usize, // not important pub nburnindex: usize, pub tx: Transaction, - pub nsidechain: usize, + pub nsidechain: usize, // not important pub strdest: String, } @@ -34,7 +48,7 @@ impl serde::Serialize for MainDeposit { blockhash: *self.blockhash.as_inner(), ntx: self.ntx, nburnindex: self.nburnindex, - tx: self.tx.serialize(), + tx: bitcoin::psbt::serialize::Serialize::serialize(&self.tx), nsidechain: self.nsidechain, strdest: self.strdest.clone(), }; @@ -50,7 +64,9 @@ impl<'de> serde::Deserialize<'de> for MainDeposit { { match SerdeMainDeposit::deserialize(deserializer) { Ok(sd) => { - let tx = match Transaction::deserialize(sd.tx.as_slice()) { + let tx = match ::deserialize( + sd.tx.as_slice(), + ) { Ok(tx) => Ok(tx), Err(err) => Err(D::Error::custom(err)), }; @@ -81,6 +97,15 @@ impl MainDeposit { Amount::from_sat(self.tx.output[self.nburnindex].value) } + pub fn spends(&self, ctip: &OutPoint) -> bool { + self.tx + .input + .iter() + .filter(|input| input.previous_output == *ctip) + .count() + == 1 + } + pub fn is_spent_by(&self, other: &MainDeposit) -> bool { other .tx @@ -92,10 +117,11 @@ impl MainDeposit { } } -/// A deposit that must be paid out in a sidechain block. +/// A deposit output. #[derive(Debug)] pub struct Deposit { - /// Sidechain address to which an `amount` of satoshi must be paid out. + /// Sidechain address without s{sidechain_number}_ prefix and checksum + /// postfix. pub address: String, /// Amount of satoshi to be deposited. pub amount: u64, diff --git a/src/drive/mod.rs b/src/drive/mod.rs index 79d1118..92cf74f 100644 --- a/src/drive/mod.rs +++ b/src/drive/mod.rs @@ -189,81 +189,6 @@ impl Drivechain { Ok(address.to_string()) } - fn update_deposits(&self, height: Option) -> Result<(), Error> { - let mut last_deposit = self - .db - .get_last_deposit()? - .map(|(_, last_deposit)| last_deposit); - trace!("updating deposits, last known deposit = {:?}", last_deposit); - while !last_deposit.clone().map_or(true, |last_deposit| { - self.client.verify_deposit(&last_deposit).unwrap_or(false) - }) { - trace!("removing invalid last deposit = {:?}", last_deposit); - self.db.remove_last_deposit()?; - last_deposit = self - .db - .get_last_deposit()? - .map(|(_, last_deposit)| last_deposit); - } - let last_output = last_deposit.map(|deposit| (deposit.tx.txid(), deposit.nburnindex)); - let deposits = self.client.get_deposits(last_output)?; - let deposits = match height { - Some(height) => { - // FIXME: Add height field to listsidechaindeposits mainchain - // RPC to get rid of this code. - let heights: HashMap = deposits - .iter() - .map(|deposit| { - Ok(( - deposit.blockhash, - self.client.get_main_block_height(&deposit.blockhash)?, - )) - }) - .collect::, Error>>()?; - deposits - .into_iter() - .filter(|deposit| heights[&deposit.blockhash] < height) - .collect() - } - None => deposits, - }; - self.db.update_deposits(deposits.as_slice())?; - let last_deposit = self - .db - .get_last_deposit()? - .map(|(_, last_deposit)| last_deposit); - trace!( - "deposits were updated, new last known deposit = {:?}", - last_deposit - ); - Ok(()) - } - - fn update_bundles(&mut self) -> Result<(), Error> { - trace!("updating bundle statuses"); - let known_failed = self.db.get_failed_bundle_hashes()?; - let failed = self.client.get_failed_withdrawal_bundle_hashes()?; - let failed = failed.difference(&known_failed); - for txid in failed { - trace!("bundle {} failed", txid); - self.db.fail_bundle(txid)?; - } - let known_spent = self.db.get_spent_bundle_hashes()?; - let spent = self.client.get_spent_withdrawal_bundle_hashes()?; - let spent = spent.difference(&known_spent); - for txid in spent { - trace!("bundle {} is spent", txid); - self.db.spend_bundle(txid)?; - } - let voting = self.client.get_voting_withdrawal_bundle_hashes()?; - for txid in voting { - trace!("bundle {} is being voted on", txid); - self.db.vote_bundle(&txid)?; - } - trace!("bundle statuses were updated successfuly"); - Ok(()) - } - // TODO: Raise alarm if bundle hash being voted on is wrong. fn attempt_bundle_broadcast(&mut self) -> Result<(), Error> { trace!("attempting to create and broadcast a new bundle"); @@ -333,6 +258,31 @@ impl Drivechain { }) } + fn update_bundles(&mut self) -> Result<(), Error> { + trace!("updating bundle statuses"); + let known_failed = self.db.get_failed_bundle_hashes()?; + let failed = self.client.get_failed_withdrawal_bundle_hashes()?; + let failed = failed.difference(&known_failed); + for txid in failed { + trace!("bundle {} failed", txid); + self.db.fail_bundle(txid)?; + } + let known_spent = self.db.get_spent_bundle_hashes()?; + let spent = self.client.get_spent_withdrawal_bundle_hashes()?; + let spent = spent.difference(&known_spent); + for txid in spent { + trace!("bundle {} is spent", txid); + self.db.spend_bundle(txid)?; + } + let voting = self.client.get_voting_withdrawal_bundle_hashes()?; + for txid in voting { + trace!("bundle {} is being voted on", txid); + self.db.vote_bundle(&txid)?; + } + trace!("bundle statuses were updated successfuly"); + Ok(()) + } + /// This must be called when a sidechain block becomes part of consensus /// (most likely when it was successfuly BMMed). pub fn connect_block( @@ -349,6 +299,56 @@ impl Drivechain { Ok(()) } + fn update_deposits(&self, height: Option) -> Result<(), Error> { + let mut last_deposit = self + .db + .get_last_deposit()? + .map(|(_, last_deposit)| last_deposit); + trace!("updating deposits, last known deposit = {:?}", last_deposit); + while !last_deposit.clone().map_or(true, |last_deposit| { + self.client.verify_deposit(&last_deposit).unwrap_or(false) + }) { + trace!("removing invalid last deposit = {:?}", last_deposit); + self.db.remove_last_deposit()?; + last_deposit = self + .db + .get_last_deposit()? + .map(|(_, last_deposit)| last_deposit); + } + let last_output = last_deposit.map(|deposit| (deposit.tx.txid(), deposit.nburnindex)); + let deposits = self.client.get_deposits(last_output)?; + let deposits = match height { + Some(height) => { + // FIXME: Add height field to listsidechaindeposits mainchain + // RPC to get rid of this code. + let heights: HashMap = deposits + .iter() + .map(|deposit| { + Ok(( + deposit.blockhash, + self.client.get_main_block_height(&deposit.blockhash)?, + )) + }) + .collect::, Error>>()?; + deposits + .into_iter() + .filter(|deposit| heights[&deposit.blockhash] < height) + .collect() + } + None => deposits, + }; + self.db.update_deposits(deposits.as_slice())?; + let last_deposit = self + .db + .get_last_deposit()? + .map(|(_, last_deposit)| last_deposit); + trace!( + "deposits were updated, new last known deposit = {:?}", + last_deposit + ); + Ok(()) + } + /// This must be called on a block when a sidechain it is no longer part of /// consensus (most likely because of a mainchain reorg). pub fn disconnect_block(