database: record whether a coin comes from an immature coinbase
We need to keep track of such coins to: - Track their maturation - Avoid using them (for instance in coin selection) Reorg handling for coinbase deposits that become immature is not implemented (yet). That's reasonable because: 1. It would be very unlikely that we'd move back, so it's most likely gonna be mature again immediately. 2. If there's a reorg of more than 100 blocks we've got bigger problems.
This commit is contained in:
parent
7ae84a22d2
commit
26add29b19
@ -66,6 +66,7 @@ fn update_coins(
|
||||
if !curr_coins.contains_key(&utxo.outpoint) {
|
||||
let coin = Coin {
|
||||
outpoint,
|
||||
is_immature: false,
|
||||
amount,
|
||||
derivation_index,
|
||||
is_change,
|
||||
|
||||
@ -976,6 +976,7 @@ mod tests {
|
||||
let mut db_conn = control.db().lock().unwrap().connection();
|
||||
db_conn.new_unspent_coins(&[Coin {
|
||||
outpoint: dummy_op,
|
||||
is_immature: false,
|
||||
block_info: None,
|
||||
amount: bitcoin::Amount::from_sat(100_000),
|
||||
derivation_index: bip32::ChildNumber::from(13),
|
||||
@ -1081,6 +1082,7 @@ mod tests {
|
||||
};
|
||||
db_conn.new_unspent_coins(&[Coin {
|
||||
outpoint: dummy_op_dup,
|
||||
is_immature: false,
|
||||
block_info: None,
|
||||
amount: bitcoin::Amount::from_sat(400_000),
|
||||
derivation_index: bip32::ChildNumber::from(42),
|
||||
@ -1127,6 +1129,7 @@ mod tests {
|
||||
db_conn.new_unspent_coins(&[
|
||||
Coin {
|
||||
outpoint: dummy_op_a,
|
||||
is_immature: false,
|
||||
block_info: None,
|
||||
amount: bitcoin::Amount::from_sat(100_000),
|
||||
derivation_index: bip32::ChildNumber::from(13),
|
||||
@ -1136,6 +1139,7 @@ mod tests {
|
||||
},
|
||||
Coin {
|
||||
outpoint: dummy_op_b,
|
||||
is_immature: false,
|
||||
block_info: None,
|
||||
amount: bitcoin::Amount::from_sat(115_680),
|
||||
derivation_index: bip32::ChildNumber::from(34),
|
||||
@ -1303,6 +1307,7 @@ mod tests {
|
||||
// Deposit 1
|
||||
Coin {
|
||||
is_change: false,
|
||||
is_immature: false,
|
||||
outpoint: OutPoint {
|
||||
txid: deposit1.txid(),
|
||||
vout: 0,
|
||||
@ -1316,6 +1321,7 @@ mod tests {
|
||||
// Deposit 2
|
||||
Coin {
|
||||
is_change: false,
|
||||
is_immature: false,
|
||||
outpoint: OutPoint {
|
||||
txid: deposit2.txid(),
|
||||
vout: 0,
|
||||
@ -1329,6 +1335,7 @@ mod tests {
|
||||
// This coin is a change output.
|
||||
Coin {
|
||||
is_change: true,
|
||||
is_immature: false,
|
||||
outpoint: OutPoint::new(spend_tx.txid(), 1),
|
||||
block_info: Some(BlockInfo { height: 3, time: 3 }),
|
||||
spend_block: None,
|
||||
@ -1339,6 +1346,7 @@ mod tests {
|
||||
// Deposit 3
|
||||
Coin {
|
||||
is_change: false,
|
||||
is_immature: false,
|
||||
outpoint: OutPoint {
|
||||
txid: deposit3.txid(),
|
||||
vout: 0,
|
||||
|
||||
@ -281,6 +281,7 @@ impl From<DbBlockInfo> for BlockInfo {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Coin {
|
||||
pub outpoint: bitcoin::OutPoint,
|
||||
pub is_immature: bool,
|
||||
pub block_info: Option<BlockInfo>,
|
||||
pub amount: bitcoin::Amount,
|
||||
pub derivation_index: bip32::ChildNumber,
|
||||
@ -293,6 +294,7 @@ impl std::convert::From<DbCoin> for Coin {
|
||||
fn from(db_coin: DbCoin) -> Coin {
|
||||
let DbCoin {
|
||||
outpoint,
|
||||
is_immature,
|
||||
block_info,
|
||||
amount,
|
||||
derivation_index,
|
||||
@ -303,6 +305,7 @@ impl std::convert::From<DbCoin> for Coin {
|
||||
} = db_coin;
|
||||
Coin {
|
||||
outpoint,
|
||||
is_immature,
|
||||
block_info: block_info.map(BlockInfo::from),
|
||||
amount,
|
||||
derivation_index,
|
||||
|
||||
@ -35,7 +35,7 @@ use miniscript::bitcoin::{
|
||||
secp256k1,
|
||||
};
|
||||
|
||||
const DB_VERSION: i64 = 1;
|
||||
const DB_VERSION: i64 = 2;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SqliteDbError {
|
||||
@ -383,8 +383,8 @@ impl SqliteConn {
|
||||
for coin in coins {
|
||||
let deriv_index: u32 = coin.derivation_index.into();
|
||||
db_tx.execute(
|
||||
"INSERT INTO coins (wallet_id, txid, vout, amount_sat, derivation_index, is_change) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
"INSERT INTO coins (wallet_id, txid, vout, amount_sat, derivation_index, is_change, is_immature) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
rusqlite::params![
|
||||
WALLET_ID,
|
||||
coin.outpoint.txid[..].to_vec(),
|
||||
@ -392,6 +392,7 @@ impl SqliteConn {
|
||||
coin.amount.to_sat(),
|
||||
deriv_index,
|
||||
coin.is_change,
|
||||
coin.is_immature,
|
||||
],
|
||||
)?;
|
||||
}
|
||||
@ -593,11 +594,12 @@ impl SqliteConn {
|
||||
.expect("Db must not fail");
|
||||
}
|
||||
|
||||
// TODO: mark coinbase deposits that were mature and became immature as such.
|
||||
/// Unconfirm all data that was marked as being confirmed *after* the given chain
|
||||
/// tip, and set it as our new best block seen.
|
||||
///
|
||||
/// This includes:
|
||||
/// - Coins
|
||||
/// - Coins (coinbase deposits that became immature isn't currently implemented)
|
||||
/// - Spending transactions confirmation
|
||||
/// - Tip
|
||||
///
|
||||
@ -823,6 +825,7 @@ CREATE TABLE spend_transactions (
|
||||
"6f0dc85a369b44458eba3a1f0ea5b5935d563afb6994f70f5b0094e05be1676c:1",
|
||||
)
|
||||
.unwrap(),
|
||||
is_immature: false,
|
||||
block_info: None,
|
||||
amount: bitcoin::Amount::from_sat(98765),
|
||||
derivation_index: bip32::ChildNumber::from_normal_idx(10).unwrap(),
|
||||
@ -855,6 +858,7 @@ CREATE TABLE spend_transactions (
|
||||
"61db3e276b095e5b05f1849dd6bfffb4e7e5ec1c4a4210099b98fce01571936f:12",
|
||||
)
|
||||
.unwrap(),
|
||||
is_immature: false,
|
||||
block_info: None,
|
||||
amount: bitcoin::Amount::from_sat(1111),
|
||||
derivation_index: bip32::ChildNumber::from_normal_idx(103).unwrap(),
|
||||
@ -949,6 +953,31 @@ CREATE TABLE spend_transactions (
|
||||
assert!(coin.spend_block.is_some());
|
||||
assert_eq!(coin.spend_block.as_ref().unwrap().time, time);
|
||||
assert_eq!(coin.spend_block.unwrap().height, height);
|
||||
|
||||
// Add an immature coin. As all coins it's first registered as unconfirmed (even though
|
||||
// it's not).
|
||||
let coin_imma = Coin {
|
||||
outpoint: bitcoin::OutPoint::from_str(
|
||||
"61db3e276b095e5b05f1849dd6bfffb4e7e5ec1c4a4210099b98fce01571937a:42",
|
||||
)
|
||||
.unwrap(),
|
||||
is_immature: true,
|
||||
block_info: None,
|
||||
amount: bitcoin::Amount::from_sat(424242),
|
||||
derivation_index: bip32::ChildNumber::from_normal_idx(4103).unwrap(),
|
||||
is_change: false, // Cannot be both a coinbase deposit and change.
|
||||
spend_txid: None,
|
||||
spend_block: None,
|
||||
};
|
||||
conn.new_unspent_coins(&[coin_imma]);
|
||||
let outpoints: HashSet<bitcoin::OutPoint> = conn
|
||||
.coins(CoinType::All)
|
||||
.into_iter()
|
||||
.map(|c| c.outpoint)
|
||||
.collect();
|
||||
assert!(outpoints.contains(&coin_imma.outpoint));
|
||||
let coin = conn.db_coins(&[coin_imma.outpoint]).pop().unwrap();
|
||||
assert!(coin.is_immature && !coin.is_change);
|
||||
}
|
||||
|
||||
fs::remove_dir_all(tmp_dir).unwrap();
|
||||
@ -1084,12 +1113,14 @@ CREATE TABLE spend_transactions (
|
||||
// - One confirmed before the rollback height but spent after
|
||||
// - One confirmed after the rollback height
|
||||
// - One spent after the rollback height
|
||||
// TODO: immature deposits
|
||||
let coins = [
|
||||
Coin {
|
||||
outpoint: bitcoin::OutPoint::from_str(
|
||||
"6f0dc85a369b44458eba3a1f0ea5b5935d563afb6994f70f5b0094e05be1676c:1",
|
||||
)
|
||||
.unwrap(),
|
||||
is_immature: false,
|
||||
block_info: None,
|
||||
amount: bitcoin::Amount::from_sat(98765),
|
||||
derivation_index: bip32::ChildNumber::from_normal_idx(10).unwrap(),
|
||||
@ -1102,6 +1133,7 @@ CREATE TABLE spend_transactions (
|
||||
"c449539458c60bee6c0d8905ba1dadb20b9187b82045d306a408b894cea492b0:2",
|
||||
)
|
||||
.unwrap(),
|
||||
is_immature: false,
|
||||
block_info: Some(BlockInfo {
|
||||
height: 101_095,
|
||||
time: 1_111_899,
|
||||
@ -1117,6 +1149,7 @@ CREATE TABLE spend_transactions (
|
||||
"f0801fd9ca8bca0624c230ab422b2e2c4c8dc995e4e1dbc6412510959cce1e4f:3",
|
||||
)
|
||||
.unwrap(),
|
||||
is_immature: false,
|
||||
block_info: Some(BlockInfo {
|
||||
height: 101_099,
|
||||
time: 1_121_899,
|
||||
@ -1140,6 +1173,7 @@ CREATE TABLE spend_transactions (
|
||||
"19f56e65069f0a7a3bfb00c6a7085cc0669e03e91befeca1ee9891c9e737b2fb:4",
|
||||
)
|
||||
.unwrap(),
|
||||
is_immature: false,
|
||||
block_info: Some(BlockInfo {
|
||||
height: 101_100,
|
||||
time: 1_131_899,
|
||||
@ -1155,6 +1189,7 @@ CREATE TABLE spend_transactions (
|
||||
"ed6c8f1af9325f84de521e785e7ddfd33dc28c9ada4d687dcd3850100bde54e9:5",
|
||||
)
|
||||
.unwrap(),
|
||||
is_immature: false,
|
||||
block_info: Some(BlockInfo {
|
||||
height: 101_102,
|
||||
time: 1_134_899,
|
||||
@ -1303,6 +1338,7 @@ CREATE TABLE spend_transactions (
|
||||
"6f0dc85a369b44458eba3a1f0ea5b5935d563afb6994f70f5b0094e05be1676c:1",
|
||||
)
|
||||
.unwrap(),
|
||||
is_immature: false,
|
||||
block_info: None,
|
||||
amount: bitcoin::Amount::from_sat(98765),
|
||||
derivation_index: bip32::ChildNumber::from_normal_idx(10).unwrap(),
|
||||
@ -1315,6 +1351,7 @@ CREATE TABLE spend_transactions (
|
||||
"c449539458c60bee6c0d8905ba1dadb20b9187b82045d306a408b894cea492b0:2",
|
||||
)
|
||||
.unwrap(),
|
||||
is_immature: false,
|
||||
block_info: Some(BlockInfo {
|
||||
height: 101_095,
|
||||
time: 1_121_000,
|
||||
@ -1330,6 +1367,7 @@ CREATE TABLE spend_transactions (
|
||||
"f0801fd9ca8bca0624c230ab422b2e2c4c8dc995e4e1dbc6412510959cce1e4f:3",
|
||||
)
|
||||
.unwrap(),
|
||||
is_immature: false,
|
||||
block_info: Some(BlockInfo {
|
||||
height: 101_099,
|
||||
time: 1_122_000,
|
||||
@ -1353,6 +1391,7 @@ CREATE TABLE spend_transactions (
|
||||
"19f56e65069f0a7a3bfb00c6a7085cc0669e03e91befeca1ee9891c9e737b2fb:4",
|
||||
)
|
||||
.unwrap(),
|
||||
is_immature: true,
|
||||
block_info: Some(BlockInfo {
|
||||
height: 101_100,
|
||||
time: 1_124_000,
|
||||
@ -1368,6 +1407,7 @@ CREATE TABLE spend_transactions (
|
||||
"ed6c8f1af9325f84de521e785e7ddfd33dc28c9ada4d687dcd3850100bde54e9:5",
|
||||
)
|
||||
.unwrap(),
|
||||
is_immature: false,
|
||||
block_info: Some(BlockInfo {
|
||||
height: 101_102,
|
||||
time: 1_125_000,
|
||||
@ -1448,7 +1488,7 @@ CREATE TABLE spend_transactions (
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v0_to_v1_migration() {
|
||||
fn v0_to_v2_migration() {
|
||||
let secp = secp256k1::Secp256k1::verification_only();
|
||||
|
||||
// Create a database with version 0, using the old schema.
|
||||
@ -1490,11 +1530,66 @@ CREATE TABLE spend_transactions (
|
||||
store_spend_old(&mut conn, &first_psbt);
|
||||
}
|
||||
|
||||
// Migrate the DB. We should be able to insert another PSBT, to query both, and the first
|
||||
// PSBT must have no associated timestamp.
|
||||
// The helper that was used to store coins in previous versions of the software, stripped
|
||||
// down to a single coin.
|
||||
fn store_coin_old(
|
||||
conn: &mut rusqlite::Connection,
|
||||
outpoint: &bitcoin::OutPoint,
|
||||
amount: bitcoin::Amount,
|
||||
derivation_index: bip32::ChildNumber,
|
||||
is_change: bool,
|
||||
) {
|
||||
db_exec(conn, |db_tx| {
|
||||
let deriv_index: u32 = derivation_index.into();
|
||||
db_tx.execute(
|
||||
"INSERT INTO coins (wallet_id, txid, vout, amount_sat, derivation_index, is_change) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
rusqlite::params![
|
||||
WALLET_ID,
|
||||
outpoint.txid[..].to_vec(),
|
||||
outpoint.vout,
|
||||
amount.to_sat(),
|
||||
deriv_index,
|
||||
is_change,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.expect("Database must be available")
|
||||
}
|
||||
|
||||
// Store a couple coins before the migration.
|
||||
{
|
||||
let mut conn = rusqlite::Connection::open(&db_path).unwrap();
|
||||
store_coin_old(
|
||||
&mut conn,
|
||||
&bitcoin::OutPoint::from_str(
|
||||
"ed6c8f1af9325f84de521e785e7ddfd33dc28c9ada4d687dcd3850100bde54e9:5",
|
||||
)
|
||||
.unwrap(),
|
||||
bitcoin::Amount::from_sat(14_000),
|
||||
24.into(),
|
||||
true,
|
||||
);
|
||||
store_coin_old(
|
||||
&mut conn,
|
||||
&bitcoin::OutPoint::from_str(
|
||||
"81b2f327d4c1fd67afd039374f8798fd9ff37932c6f5c221c1c569350eac5ac8:2",
|
||||
)
|
||||
.unwrap(),
|
||||
bitcoin::Amount::from_sat(392_093_123),
|
||||
24_567.into(),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
// Migrate the DB.
|
||||
maybe_apply_migration(&db_path).unwrap();
|
||||
maybe_apply_migration(&db_path).unwrap(); // Migrating twice will be a no-op.
|
||||
let db = SqliteDb::new(db_path, None, &secp).unwrap();
|
||||
|
||||
// We should now be able to insert another PSBT, to query both, and the first PSBT must
|
||||
// have no associated timestamp.
|
||||
{
|
||||
let mut conn = db.connection().unwrap();
|
||||
conn.store_spend(&second_psbt);
|
||||
@ -1511,6 +1606,28 @@ CREATE TABLE spend_transactions (
|
||||
assert!(second_spend.updated_at.is_some());
|
||||
}
|
||||
|
||||
// We should now be able to store an immature coin, query all of them, and the first two
|
||||
// should not be immature.
|
||||
{
|
||||
let mut conn = db.connection().unwrap();
|
||||
conn.new_unspent_coins(&[Coin {
|
||||
outpoint: bitcoin::OutPoint::from_str(
|
||||
"6f0dc85a369b44458eba3a1f0ea5b5935d563afb6994f70f5b0094e05be1676c:1",
|
||||
)
|
||||
.unwrap(),
|
||||
is_immature: true,
|
||||
block_info: None,
|
||||
amount: bitcoin::Amount::from_sat(98765),
|
||||
derivation_index: bip32::ChildNumber::from_normal_idx(10).unwrap(),
|
||||
is_change: false,
|
||||
spend_txid: None,
|
||||
spend_block: None,
|
||||
}]);
|
||||
let coins = conn.coins(CoinType::All);
|
||||
assert_eq!(coins.len(), 3);
|
||||
assert_eq!(coins.iter().filter(|c| !c.is_immature).count(), 2);
|
||||
}
|
||||
|
||||
fs::remove_dir_all(tmp_dir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,6 +39,10 @@ CREATE TABLE wallets (
|
||||
*
|
||||
* The 'spend_block_height' and 'spend_block.time' are only present if the spending
|
||||
* transaction for this coin exists and was confirmed.
|
||||
*
|
||||
* The 'is_immature' field is for coinbase deposits that are not yet buried under 100
|
||||
* blocks. Note coinbase deposits can't be change. They also technically can't be
|
||||
* unconfirmed but we keep them as such until they become mature.
|
||||
*/
|
||||
CREATE TABLE coins (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
@ -53,6 +57,8 @@ CREATE TABLE coins (
|
||||
spend_txid BLOB,
|
||||
spend_block_height INTEGER,
|
||||
spend_block_time INTEGER,
|
||||
is_immature BOOLEAN NOT NULL CHECK (is_immature IN (0,1)),
|
||||
CHECK (is_change IS 0 OR is_immature IS 0),
|
||||
UNIQUE (txid, vout),
|
||||
FOREIGN KEY (wallet_id) REFERENCES wallets (id)
|
||||
ON UPDATE RESTRICT
|
||||
@ -156,6 +162,8 @@ pub struct DbBlockInfo {
|
||||
pub struct DbCoin {
|
||||
pub id: i64,
|
||||
pub wallet_id: i64,
|
||||
/// Whether this coin was created by a yet-to-be-mature coinbase transaction.
|
||||
pub is_immature: bool,
|
||||
pub outpoint: bitcoin::OutPoint,
|
||||
pub block_info: Option<DbBlockInfo>,
|
||||
pub amount: bitcoin::Amount,
|
||||
@ -201,9 +209,16 @@ impl TryFrom<&rusqlite::Row<'_>> for DbCoin {
|
||||
time: spend_time.expect("Must be there if height is"),
|
||||
});
|
||||
|
||||
let is_immature: bool = row.get(12)?;
|
||||
assert!(
|
||||
!is_immature || !is_change,
|
||||
"A coin cannot be both created in a coinbase and be change"
|
||||
);
|
||||
|
||||
Ok(DbCoin {
|
||||
id,
|
||||
wallet_id,
|
||||
is_immature,
|
||||
outpoint,
|
||||
block_info,
|
||||
amount,
|
||||
|
||||
@ -165,6 +165,25 @@ fn migrate_v0_to_v1(conn: &mut rusqlite::Connection) -> Result<(), SqliteDbError
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// After Liana 1.0 we upgraded the schema to record whether a coin originated from an immature
|
||||
// coinbase transaction.
|
||||
fn migrate_v1_to_v2(conn: &mut rusqlite::Connection) -> Result<(), SqliteDbError> {
|
||||
db_exec(conn, |tx| {
|
||||
tx.execute(
|
||||
"ALTER TABLE coins ADD COLUMN is_immature",
|
||||
rusqlite::params![],
|
||||
)?;
|
||||
tx.execute(
|
||||
"UPDATE coins SET is_immature = 0 WHERE is_immature IS NULL",
|
||||
rusqlite::params![],
|
||||
)?;
|
||||
tx.execute("UPDATE version SET version = 2", rusqlite::params![])?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check the database version and if necessary apply the migrations to upgrade it to the current
|
||||
/// one.
|
||||
pub fn maybe_apply_migration(db_path: &path::Path) -> Result<(), SqliteDbError> {
|
||||
@ -183,6 +202,11 @@ pub fn maybe_apply_migration(db_path: &path::Path) -> Result<(), SqliteDbError>
|
||||
migrate_v0_to_v1(&mut conn)?;
|
||||
log::warn!("Migration from database version 0 to version 1 successful.");
|
||||
}
|
||||
1 => {
|
||||
log::warn!("Upgrading database from version 1 to version 2.");
|
||||
migrate_v1_to_v2(&mut conn)?;
|
||||
log::warn!("Migration from database version 1 to version 2 successful.");
|
||||
}
|
||||
_ => return Err(SqliteDbError::UnsupportedVersion(version)),
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user