diff --git a/src/bitcoin/d/mod.rs b/src/bitcoin/d/mod.rs index 9d3d670c..160ecd0a 100644 --- a/src/bitcoin/d/mod.rs +++ b/src/bitcoin/d/mod.rs @@ -1105,6 +1105,7 @@ pub struct LSBlockEntry { pub block_height: Option, pub address: bitcoin::Address, pub parent_descs: Vec>, + pub is_immature: bool, } impl From<&Json> for LSBlockEntry { @@ -1150,12 +1151,19 @@ impl From<&Json> for LSBlockEntry { }) .expect("bitcoind can't give invalid descriptors"); + let is_immature = json + .get("category") + .and_then(Json::as_str) + .expect("must be present") + == "immature"; + LSBlockEntry { outpoint, amount, block_height, address, parent_descs, + is_immature, } } } @@ -1183,7 +1191,7 @@ impl From for LSBlockRes { .get("category") .and_then(Json::as_str) .expect("must be present"); - if category == "receive" || category == "generate" { + if ["receive", "generate", "immature"].contains(&category) { let lsb_entry: LSBlockEntry = j.into(); Some(lsb_entry) } else { @@ -1201,6 +1209,8 @@ pub struct GetTxRes { pub conflicting_txs: Vec, pub block: Option, pub tx: bitcoin::Transaction, + pub is_coinbase: bool, + pub confirmations: i32, } impl From for GetTxRes { @@ -1238,10 +1248,21 @@ impl From for GetTxRes { let bytes = Vec::from_hex(hex).expect("bitcoind returned a wrong transaction format"); let tx: bitcoin::Transaction = bitcoin::consensus::encode::deserialize(&bytes) .expect("bitcoind returned a wrong transaction format"); + let is_coinbase = json + .get("generated") + .and_then(Json::as_bool) + .unwrap_or(false); + let confirmations = json + .get("confirmations") + .and_then(Json::as_i64) + .expect("Must be present in the response") as i32; + GetTxRes { conflicting_txs: conflicting_txs.unwrap_or_default(), block, tx, + is_coinbase, + confirmations, } } } diff --git a/src/bitcoin/mod.rs b/src/bitcoin/mod.rs index a3b9e01a..b1d32fae 100644 --- a/src/bitcoin/mod.rs +++ b/src/bitcoin/mod.rs @@ -14,6 +14,8 @@ use std::{fmt, sync}; use miniscript::bitcoin::{self, address}; +const COINBASE_MATURITY: i32 = 100; + /// Information about a block #[derive(Debug, Clone, Eq, PartialEq, Copy)] pub struct Block { @@ -146,6 +148,7 @@ impl BitcoinInterface for d::BitcoinD { block_height, address, parent_descs, + is_immature, } = entry; if parent_descs .iter() @@ -156,6 +159,7 @@ impl BitcoinInterface for d::BitcoinD { amount, block_height, address, + is_immature, }) } else { None @@ -184,6 +188,11 @@ impl BitcoinInterface for d::BitcoinD { // If the transaction was confirmed, mark the coin as such. if let Some(block) = res.block { + // Do not mark immature coinbase deposits as confirmed until they become mature. + if res.is_coinbase && res.confirmations < COINBASE_MATURITY { + log::debug!("Coin at '{}' comes from an immature coinbase transaction with {} confirmations. Not marking it as confirmed for now.", op, res.confirmations); + continue; + } confirmed.push((*op, block.height, block.time)); continue; } @@ -409,4 +418,5 @@ pub struct UTxO { pub amount: bitcoin::Amount, pub block_height: Option, pub address: bitcoin::Address, + pub is_immature: bool, } diff --git a/src/bitcoin/poller/looper.rs b/src/bitcoin/poller/looper.rs index 8944d92c..a1726780 100644 --- a/src/bitcoin/poller/looper.rs +++ b/src/bitcoin/poller/looper.rs @@ -24,6 +24,8 @@ struct UpdatedCoins { // or spent. // NOTE: A coin may be updated multiple times at once. That is, a coin may be received, confirmed, // and spent in a single poll. +// NOTE: Coinbase transaction deposits are very much an afterthought here. We treat them as +// unconfirmed until the CB tx matures. fn update_coins( bit: &impl BitcoinInterface, db_conn: &mut Box, @@ -42,6 +44,7 @@ fn update_coins( outpoint, amount, address, + is_immature, .. } = utxo; // We can only really treat them if we know the derivation index that was used. @@ -66,7 +69,7 @@ fn update_coins( if !curr_coins.contains_key(&utxo.outpoint) { let coin = Coin { outpoint, - is_immature: false, + is_immature, amount, derivation_index, is_change, diff --git a/src/database/mod.rs b/src/database/mod.rs index e25acb92..21b5a32d 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -91,6 +91,9 @@ pub trait DatabaseConnection { fn remove_coins(&mut self, coins: &[bitcoin::OutPoint]); /// Mark a set of coins as being confirmed at a specified height and block time. + /// NOTE: if the coin comes from an immature coinbase transaction, this will mark it as mature. + /// Immature coinbase deposits must not be confirmed before they are 100 blocks deep in the + /// chain. fn confirm_coins(&mut self, outpoints: &[(bitcoin::OutPoint, i32, u32)]); /// Mark a set of coins as being spent by a specified txid of a pending transaction. diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs index 2cf3aa7b..d05e4663 100644 --- a/src/database/sqlite/mod.rs +++ b/src/database/sqlite/mod.rs @@ -417,6 +417,9 @@ impl SqliteConn { } /// Mark a set of coins as confirmed. + /// + /// NOTE: this will also mark the coin as mature if it originates from an immature coinbase + /// deposit. pub fn confirm_coins<'a>( &mut self, outpoints: impl IntoIterator, @@ -424,7 +427,7 @@ impl SqliteConn { db_exec(&mut self.conn, |db_tx| { for (outpoint, height, time) in outpoints { db_tx.execute( - "UPDATE coins SET blockheight = ?1, blocktime = ?2 WHERE txid = ?3 AND vout = ?4", + "UPDATE coins SET blockheight = ?1, blocktime = ?2, is_immature = 0 WHERE txid = ?3 AND vout = ?4", rusqlite::params![height, time, outpoint.txid[..].to_vec(), outpoint.vout,], )?; } @@ -978,6 +981,12 @@ CREATE TABLE spend_transactions ( assert!(outpoints.contains(&coin_imma.outpoint)); let coin = conn.db_coins(&[coin_imma.outpoint]).pop().unwrap(); assert!(coin.is_immature && !coin.is_change); + + // Confirming an immature coin marks it as mature. + let (height, time) = (424242, 424241); + conn.confirm_coins(&[(coin_imma.outpoint, height, time)]); + let coin = conn.db_coins(&[coin_imma.outpoint]).pop().unwrap(); + assert!(!coin.is_immature); } fs::remove_dir_all(tmp_dir).unwrap();