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:
parent
f2c910f6eb
commit
4f6dcbfdfd
@ -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(&[], &[]);
|
||||
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user