sqlite: add columns to transactions and coins tables

The new columns in the transactions table will be populated for
new transactions by the poller, while existing rows will be
updated in a subsequent migration.

New and existing coins will all have `is_from_self` set to false
due to the default column value.

None of these columns will be used by the wallet at this stage.
This commit is contained in:
Michael Mallan 2024-11-28 09:03:08 +00:00
parent f2c910f6eb
commit 4f6dcbfdfd
No known key found for this signature in database
GPG Key ID: 5177CDCEDB0EABEB
3 changed files with 77 additions and 26 deletions

View File

@ -43,7 +43,7 @@ use miniscript::bitcoin::{
secp256k1,
};
const DB_VERSION: i64 = 6;
const DB_VERSION: i64 = 7;
/// Last database version for which Bitcoin transactions were not stored in database. In practice
/// this meant we relied on the bitcoind watchonly wallet to store them for us.
@ -733,9 +733,16 @@ impl SqliteConn {
let txid = &tx.txid()[..].to_vec();
let tx_ser = bitcoin::consensus::serialize(tx);
db_tx.execute(
"INSERT INTO transactions (txid, tx) VALUES (?1, ?2) \
"INSERT INTO transactions (txid, tx, num_inputs, num_outputs, is_coinbase) \
VALUES (?1, ?2, ?3, ?4, ?5) \
ON CONFLICT DO NOTHING",
rusqlite::params![txid, tx_ser,],
rusqlite::params![
txid,
tx_ser,
tx.input.len(),
tx.output.len(),
tx.is_coinbase()
],
)?;
}
Ok(())
@ -840,7 +847,7 @@ mod tests {
str::FromStr,
};
use bitcoin::bip32;
use bitcoin::{bip32, ScriptBuf};
// The database schema used by the first versions of Liana (database version 0). Used to test
// migrations starting from the first version.
@ -1299,8 +1306,8 @@ CREATE TABLE labels (
.map(|i| bitcoin::Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::absolute::LockTime::from_height(i).unwrap(),
input: Vec::new(),
output: Vec::new(),
input: vec![bitcoin::TxIn::default()], // a single input
output: vec![bitcoin::TxOut::minimal_non_dust(ScriptBuf::default())], // a single output,
})
.collect();
conn.new_txs(&txs);
@ -1602,8 +1609,8 @@ CREATE TABLE labels (
.map(|i| bitcoin::Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::absolute::LockTime::from_height(i).unwrap(),
input: Vec::new(),
output: Vec::new(),
input: vec![bitcoin::TxIn::default()], // a single input
output: vec![bitcoin::TxOut::minimal_non_dust(ScriptBuf::default())], // a single output,
})
.collect();
conn.new_txs(&txs);
@ -1935,8 +1942,8 @@ CREATE TABLE labels (
.map(|i| bitcoin::Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::absolute::LockTime::from_height(i).unwrap(),
input: Vec::new(),
output: Vec::new(),
input: vec![bitcoin::TxIn::default()], // a single input
output: vec![bitcoin::TxOut::minimal_non_dust(ScriptBuf::default())], // a single output,
})
.collect();
conn.new_txs(&txs);
@ -2144,8 +2151,8 @@ CREATE TABLE labels (
.map(|i| bitcoin::Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::absolute::LockTime::from_height(i).unwrap(),
input: Vec::new(),
output: Vec::new(),
input: vec![bitcoin::TxIn::default()], // a single input
output: vec![bitcoin::TxOut::minimal_non_dust(ScriptBuf::default())], // a single output,
})
.collect();
conn.new_txs(&txs);
@ -2263,8 +2270,8 @@ CREATE TABLE labels (
.map(|i| bitcoin::Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::absolute::LockTime::from_height(i).unwrap(),
input: Vec::new(),
output: Vec::new(),
input: vec![bitcoin::TxIn::default()], // a single input
output: vec![bitcoin::TxOut::minimal_non_dust(ScriptBuf::default())], // a single output,
})
.collect();
conn.new_txs(&txs);
@ -2291,8 +2298,8 @@ CREATE TABLE labels (
.map(|i| bitcoin::Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::absolute::LockTime::from_height(i).unwrap(),
input: Vec::new(),
output: Vec::new(),
input: vec![bitcoin::TxIn::default()], // a single input
output: vec![bitcoin::TxOut::minimal_non_dust(ScriptBuf::default())], // a single output,
})
.collect();
let spend_txs: Vec<_> = (0..10)
@ -2301,8 +2308,8 @@ CREATE TABLE labels (
bitcoin::Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::absolute::LockTime::from_height(1_234 + i).unwrap(),
input: Vec::new(),
output: Vec::new(),
input: vec![bitcoin::TxIn::default()], // a single input
output: vec![bitcoin::TxOut::minimal_non_dust(ScriptBuf::default())], // a single output,
},
if i % 2 == 0 {
Some(BlockInfo {
@ -2462,7 +2469,7 @@ CREATE TABLE labels (
}
#[test]
fn v0_to_v6_migration() {
fn v0_to_v7_migration() {
let secp = secp256k1::Secp256k1::verification_only();
// Create a database with version 0, using the old schema.
@ -2568,7 +2575,7 @@ CREATE TABLE labels (
{
let mut conn = db.connection().unwrap();
let version = conn.db_version();
assert_eq!(version, 6);
assert_eq!(version, 7);
}
// We should now be able to insert another PSBT, to query both, and the first PSBT must
// have no associated timestamp.
@ -2595,8 +2602,8 @@ CREATE TABLE labels (
let tx = bitcoin::Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::absolute::LockTime::from_height(2).unwrap(),
input: Vec::new(),
output: Vec::new(),
input: vec![bitcoin::TxIn::default()], // a single input
output: vec![bitcoin::TxOut::minimal_non_dust(ScriptBuf::default())], // a single output,
};
conn.new_txs(&[tx.clone()]);
conn.new_unspent_coins(&[Coin {
@ -2642,7 +2649,7 @@ CREATE TABLE labels (
}
#[test]
fn v3_to_v6_migration() {
fn v3_to_v7_migration() {
let secp = secp256k1::Secp256k1::verification_only();
// Create a database with version 3, using the old schema.
@ -2792,10 +2799,10 @@ CREATE TABLE labels (
// Migrate the DB.
maybe_apply_migration(&db_path, &bitcoin_txs).unwrap();
assert_eq!(conn.db_version(), 6);
assert_eq!(conn.db_version(), 7);
// Migrating twice will be a no-op. No need to pass `bitcoin_txs` second time.
maybe_apply_migration(&db_path, &[]).unwrap();
assert!(conn.db_version() == 6);
assert!(conn.db_version() == 7);
// Compare the `DbCoin`s with the expected values.
let coins_post = conn.coins(&[], &[]);

View File

@ -4,6 +4,11 @@ use std::{convert::TryFrom, str::FromStr};
use miniscript::bitcoin::{self, address, bip32, consensus::encode, psbt::Psbt};
// Due to limitations of Sqlite's ALTER TABLE command and in order not to recreate
// tables during migration:
// - Columns `num_inputs` and `num_outputs` of the transactions table remain nullable.
// - There is no CHECK constraint to prevent both `is_immature` and `is_from_self`
// being true in the coins table.
pub const SCHEMA: &str = "\
CREATE TABLE version (
version INTEGER NOT NULL
@ -42,6 +47,10 @@ CREATE TABLE wallets (
* The 'is_immature' field is for coinbase deposits that are not yet buried under 100
* blocks. Note coinbase deposits can't technically be unconfirmed but we keep them
* as such until they become mature.
*
* The `is_from_self` field indicates if the coin is the output of a transaction whose
* inputs are all from the same wallet as the coin. For an unconfirmed coin, this also
* means that all unconfirmed ancestors, if any, are from self.
*/
CREATE TABLE coins (
id INTEGER PRIMARY KEY NOT NULL,
@ -57,6 +66,7 @@ CREATE TABLE coins (
spend_block_height INTEGER,
spend_block_time INTEGER,
is_immature BOOLEAN NOT NULL CHECK (is_immature IN (0,1)),
is_from_self BOOLEAN NOT NULL DEFAULT 0 CHECK (is_from_self IN (0,1)),
UNIQUE (txid, vout),
FOREIGN KEY (wallet_id) REFERENCES wallets (id)
ON UPDATE RESTRICT
@ -82,7 +92,10 @@ CREATE TABLE addresses (
CREATE TABLE transactions (
id INTEGER PRIMARY KEY NOT NULL,
txid BLOB UNIQUE NOT NULL,
tx BLOB UNIQUE NOT NULL
tx BLOB UNIQUE NOT NULL,
num_inputs INTEGER CHECK (num_inputs IS NULL OR num_inputs > 0),
num_outputs INTEGER CHECK (num_outputs IS NULL OR num_outputs > 0),
is_coinbase BOOLEAN NOT NULL DEFAULT 0 CHECK (is_coinbase IN (0,1))
);
/* Transactions we created that spend some of our coins. */
@ -195,6 +208,12 @@ pub struct DbCoin {
pub is_change: bool,
pub spend_txid: Option<bitcoin::Txid>,
pub spend_block: Option<DbBlockInfo>,
/// A coin is from self if it is the output of a transaction whose
/// inputs are all from this wallet. For unconfirmed coins, we
/// further require that all unconfirmed ancestors, if any, also
/// be from self, as otherwise they will depend on an unconfirmed
/// external transaction.
pub is_from_self: bool,
}
impl TryFrom<&rusqlite::Row<'_>> for DbCoin {
@ -234,6 +253,7 @@ impl TryFrom<&rusqlite::Row<'_>> for DbCoin {
});
let is_immature: bool = row.get(12)?;
let is_from_self: bool = row.get(13)?;
Ok(DbCoin {
id,
@ -246,6 +266,7 @@ impl TryFrom<&rusqlite::Row<'_>> for DbCoin {
is_change,
spend_txid,
spend_block,
is_from_self,
})
}
}

View File

@ -307,6 +307,24 @@ fn migrate_v5_to_v6(conn: &mut rusqlite::Connection) -> Result<(), SqliteDbError
Ok(())
}
fn migrate_v6_to_v7(conn: &mut rusqlite::Connection) -> Result<(), SqliteDbError> {
db_exec(conn, |db_tx| {
db_tx.execute_batch(
"
ALTER TABLE transactions ADD COLUMN num_inputs INTEGER CHECK (num_inputs IS NULL OR num_inputs > 0);
ALTER TABLE transactions ADD COLUMN num_outputs INTEGER CHECK (num_outputs IS NULL OR num_outputs > 0);
ALTER TABLE transactions ADD COLUMN is_coinbase BOOLEAN NOT NULL DEFAULT 0 CHECK (is_coinbase IN (0,1));
ALTER TABLE coins ADD COLUMN is_from_self BOOLEAN NOT NULL DEFAULT 0 CHECK (is_from_self IN (0,1));
UPDATE version SET version = 7;
",
)?;
Ok(())
})?;
Ok(())
}
/// Check the database version and if necessary apply the migrations to upgrade it to the current
/// one. The `bitcoin_txs` parameter is here for the migration from versions 4 and earlier, which
/// did not store the Bitcoin transactions in database, to versions 5 and later, which do. For a
@ -360,6 +378,11 @@ pub fn maybe_apply_migration(
migrate_v5_to_v6(&mut conn)?;
log::warn!("Migration from database version 5 to version 6 successful.");
}
6 => {
log::warn!("Upgrading database from version 6 to version 7.");
migrate_v6_to_v7(&mut conn)?;
log::warn!("Migration from database version 6 to version 7 successful.");
}
_ => return Err(SqliteDbError::UnsupportedVersion(version)),
}
}