From bafcadf39870c02b89a9a4627a6918fce6c54f30 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Thu, 15 Sep 2022 11:33:43 +0200 Subject: [PATCH] db: interface to upsert a Spend PSBT and query it by txid I initially wanted to have a bridge table between the 'coins' and 'spend_transactions' table as we do in revaultd. But let's not optimize to early, we'll see if/when we need it. --- src/database/mod.rs | 18 ++++++++++++++++- src/database/sqlite/mod.rs | 34 +++++++++++++++++++++++++++++-- src/database/sqlite/schema.rs | 38 ++++++++++++++++++++++++++++++++++- src/testutils.rs | 20 +++++++++++++++++- 4 files changed, 105 insertions(+), 5 deletions(-) diff --git a/src/database/mod.rs b/src/database/mod.rs index 4a7afc30..2bf5214b 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -13,7 +13,10 @@ use crate::{ use std::{collections::HashMap, sync}; -use miniscript::bitcoin::{self, secp256k1, util::bip32}; +use miniscript::bitcoin::{ + self, secp256k1, + util::{bip32, psbt::PartiallySignedTransaction as Psbt}, +}; pub trait DatabaseInterface: Send { fn connection(&self) -> Box; @@ -68,6 +71,11 @@ pub trait DatabaseConnection { &mut self, outpoints: &[bitcoin::OutPoint], ) -> HashMap; + + fn spend_tx(&mut self, txid: &bitcoin::Txid) -> Option; + + /// Insert a new Spend transaction or replace an existing one. + fn store_spend(&mut self, psbt: &Psbt); } // FIXME: if possible, avoid reallocating. @@ -155,6 +163,14 @@ impl DatabaseConnection for SqliteConn { ) -> HashMap { db_coins_into_coins(self.db_coins(outpoints)) } + + fn spend_tx(&mut self, txid: &bitcoin::Txid) -> Option { + self.db_spend(txid).map(|db_spend| db_spend.psbt) + } + + fn store_spend(&mut self, psbt: &Psbt) { + self.store_spend(psbt) + } } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/src/database/sqlite/mod.rs b/src/database/sqlite/mod.rs index 373de446..55b76171 100644 --- a/src/database/sqlite/mod.rs +++ b/src/database/sqlite/mod.rs @@ -13,7 +13,7 @@ use crate::{ bitcoin::BlockChainTip, database::{ sqlite::{ - schema::{DbAddress, DbCoin, DbTip, DbWallet}, + schema::{DbAddress, DbCoin, DbSpendTransaction, DbTip, DbWallet}, utils::{create_fresh_db, db_exec, db_query, db_tx_query, LOOK_AHEAD_LIMIT}, }, Coin, @@ -23,7 +23,10 @@ use crate::{ use std::{convert::TryInto, fmt, io, path}; -use miniscript::bitcoin::{self, hashes::hex::ToHex, secp256k1}; +use miniscript::bitcoin::{ + self, consensus::encode, hashes::hex::ToHex, secp256k1, + util::psbt::PartiallySignedTransaction as Psbt, +}; const DB_VERSION: i64 = 0; @@ -352,6 +355,33 @@ impl SqliteConn { }) .expect("Db must not fail") } + + pub fn db_spend(&mut self, txid: &bitcoin::Txid) -> Option { + db_query( + &mut self.conn, + "SELECT * FROM spend_transactions WHERE txid = ?1", + rusqlite::params![txid.to_vec()], + |row| row.try_into(), + ) + .expect("Db must not fail") + .pop() + } + + /// Insert a new Spend transaction or replace an existing one. + pub fn store_spend(&mut self, psbt: &Psbt) { + let txid = psbt.global.unsigned_tx.txid().to_vec(); + let psbt = encode::serialize(psbt); + + db_exec(&mut self.conn, |db_tx| { + db_tx.execute( + "INSERT into spend_transactions (psbt, txid) VALUES (?1, ?2) \ + ON CONFLICT DO UPDATE SET psbt=excluded.psbt", + rusqlite::params![psbt, txid], + )?; + Ok(()) + }) + .expect("Db must not fail"); + } } #[cfg(test)] diff --git a/src/database/sqlite/schema.rs b/src/database/sqlite/schema.rs index 355728fe..fe6b662f 100644 --- a/src/database/sqlite/schema.rs +++ b/src/database/sqlite/schema.rs @@ -2,7 +2,11 @@ use crate::descriptors::InheritanceDescriptor; use std::{convert::TryFrom, str::FromStr}; -use miniscript::bitcoin::{self, consensus::encode, util::bip32}; +use miniscript::bitcoin::{ + self, + consensus::encode, + util::{bip32, psbt::PartiallySignedTransaction as Psbt}, +}; pub const SCHEMA: &str = "\ CREATE TABLE version ( @@ -49,6 +53,13 @@ CREATE TABLE addresses ( address TEXT NOT NULL UNIQUE, derivation_index INTEGER NOT NULL UNIQUE ); + +/* Transactions we created that spend some of our coins. */ +CREATE TABLE spend_transactions ( + id INTEGER PRIMARY KEY NOT NULL, + psbt BLOB UNIQUE NOT NULL, + txid BLOB UNIQUE NOT NULL +); "; /// A row in the "tip" table. @@ -186,3 +197,28 @@ impl TryFrom<&rusqlite::Row<'_>> for DbAddress { }) } } + +/// A row in the "spend_transactions" table +#[derive(Debug, Clone, PartialEq)] +pub struct DbSpendTransaction { + pub id: i64, + pub psbt: Psbt, + pub txid: bitcoin::Txid, +} + +impl TryFrom<&rusqlite::Row<'_>> for DbSpendTransaction { + type Error = rusqlite::Error; + + fn try_from(row: &rusqlite::Row) -> Result { + let id: i64 = row.get(0)?; + + let psbt: Vec = row.get(1)?; + let psbt: Psbt = encode::deserialize(&psbt).expect("We only store valid PSBTs"); + + let txid: Vec = row.get(2)?; + let txid: bitcoin::Txid = encode::deserialize(&txid).expect("We only store valid txids"); + assert_eq!(txid, psbt.global.unsigned_tx.txid()); + + Ok(DbSpendTransaction { id, psbt, txid }) + } +} diff --git a/src/testutils.rs b/src/testutils.rs index 27d34775..5cefaf93 100644 --- a/src/testutils.rs +++ b/src/testutils.rs @@ -8,7 +8,10 @@ use crate::{ use std::{collections::HashMap, env, fs, io, path, process, str::FromStr, sync, thread, time}; use miniscript::{ - bitcoin::{self, secp256k1, util::bip32}, + bitcoin::{ + self, secp256k1, + util::{bip32, psbt::PartiallySignedTransaction as Psbt}, + }, descriptor, }; @@ -58,6 +61,7 @@ pub struct DummyDb { curr_index: bip32::ChildNumber, curr_tip: Option, coins: HashMap, + spend_txs: HashMap, } impl DummyDb { @@ -66,6 +70,7 @@ impl DummyDb { curr_index: 0.into(), curr_tip: None, coins: HashMap::new(), + spend_txs: HashMap::new(), } } } @@ -152,6 +157,19 @@ impl DatabaseConnection for DummyDbConn { .filter(|(op, _)| outpoints.contains(&op)) .collect() } + + fn store_spend(&mut self, psbt: &Psbt) { + let txid = psbt.global.unsigned_tx.txid(); + self.db + .write() + .unwrap() + .spend_txs + .insert(txid, psbt.clone()); + } + + fn spend_tx(&mut self, txid: &bitcoin::Txid) -> Option { + self.db.read().unwrap().spend_txs.get(txid).cloned() + } } pub struct DummyMinisafe {