Add blocktime and spent_at to coins table

Spent coins are coins where spent_txid and spent_at
are both not null.
This commit is contained in:
edouard 2022-09-16 15:25:51 +02:00
parent ac6e0443ea
commit 94ee94edbd
8 changed files with 137 additions and 66 deletions

View File

@ -751,6 +751,7 @@ impl From<Json> for LSBlockRes {
#[derive(Debug, Clone, Copy)]
pub struct GetTxRes {
pub block_height: Option<i32>,
pub block_time: Option<u32>,
}
impl From<Json> for GetTxRes {
@ -759,6 +760,13 @@ impl From<Json> for GetTxRes {
.get("blockheight")
.and_then(Json::as_i64)
.map(|bh| bh as i32);
GetTxRes { block_height }
let block_time = json
.get("blocktime")
.and_then(Json::as_u64)
.map(|bt| bt as u32);
GetTxRes {
block_height,
block_time,
}
}
}

View File

@ -34,11 +34,14 @@ pub trait BitcoinInterface: Send {
/// Get coins received since the specified tip.
fn received_coins(&self, tip: &BlockChainTip) -> Vec<UTxO>;
/// Get all coins that were confirmed, and at what height.
fn confirmed_coins(&self, outpoints: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, i32)>;
/// Get all coins that were confirmed, and at what height and time.
fn confirmed_coins(
&self,
outpoints: &[bitcoin::OutPoint],
) -> Vec<(bitcoin::OutPoint, i32, u32)>;
/// Get all coins that were spent, and the spending txid.
fn spent_coins(
/// Get all coins that are being spent, and the spending txid.
fn spending_coins(
&self,
outpoints: &[bitcoin::OutPoint],
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)>;
@ -91,14 +94,19 @@ impl BitcoinInterface for d::BitcoinD {
.collect()
}
fn confirmed_coins(&self, outpoints: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, i32)> {
fn confirmed_coins(
&self,
outpoints: &[bitcoin::OutPoint],
) -> Vec<(bitcoin::OutPoint, i32, u32)> {
let mut confirmed = Vec::with_capacity(outpoints.len());
for op in outpoints {
// TODO: batch those calls to gettransaction
if let Some(res) = self.get_transaction(&op.txid) {
if let Some(h) = res.block_height {
confirmed.push((*op, h));
if let Some(t) = res.block_time {
confirmed.push((*op, h, t));
}
}
} else {
log::error!("Transaction not in wallet for coin '{}'.", op);
@ -108,7 +116,7 @@ impl BitcoinInterface for d::BitcoinD {
confirmed
}
fn spent_coins(
fn spending_coins(
&self,
outpoints: &[bitcoin::OutPoint],
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
@ -126,6 +134,7 @@ impl BitcoinInterface for d::BitcoinD {
);
bitcoin::Txid::from_slice(&[0; 32][..]).unwrap()
};
spent.push((*op, spending_txid));
}
}
@ -156,15 +165,18 @@ impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>>
self.lock().unwrap().received_coins(tip)
}
fn confirmed_coins(&self, outpoints: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, i32)> {
fn confirmed_coins(
&self,
outpoints: &[bitcoin::OutPoint],
) -> Vec<(bitcoin::OutPoint, i32, u32)> {
self.lock().unwrap().confirmed_coins(outpoints)
}
fn spent_coins(
fn spending_coins(
&self,
outpoints: &[bitcoin::OutPoint],
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
self.lock().unwrap().spent_coins(outpoints)
self.lock().unwrap().spending_coins(outpoints)
}
}

View File

@ -13,8 +13,8 @@ use miniscript::bitcoin;
#[derive(Debug, Clone)]
struct UpdatedCoins {
pub received: Vec<Coin>,
pub confirmed: Vec<(bitcoin::OutPoint, i32)>,
pub spent: Vec<(bitcoin::OutPoint, bitcoin::Txid)>,
pub confirmed: Vec<(bitcoin::OutPoint, i32, u32)>,
pub spending: Vec<(bitcoin::OutPoint, bitcoin::Txid)>,
}
// Update the state of our coins. There may be new unspent, and existing ones may become confirmed
@ -40,7 +40,9 @@ fn update_coins(
amount,
derivation_index,
block_height: None,
block_time: None,
spend_txid: None,
spent_at: None,
};
received.push(coin);
}
@ -83,12 +85,12 @@ fn update_coins(
}
})
.collect();
let spent = bit.spent_coins(&to_be_spent);
let spending = bit.spending_coins(&to_be_spent);
UpdatedCoins {
received,
confirmed,
spent,
spending,
}
}
@ -136,7 +138,7 @@ fn updates(bit: &impl BitcoinInterface, db: &impl DatabaseInterface) {
// updates up to this block. But not more.
db_conn.new_unspent_coins(&updated_coins.received);
db_conn.confirm_coins(&updated_coins.confirmed);
db_conn.spend_coins(&updated_coins.spent);
db_conn.spend_coins(&updated_coins.spending);
if let Some(tip) = new_tip {
db_conn.update_tip(&tip);
}

View File

@ -568,9 +568,11 @@ mod tests {
db_conn.new_unspent_coins(&[Coin {
outpoint: dummy_op,
block_height: None,
block_time: None,
amount: bitcoin::Amount::from_sat(100_000),
derivation_index: bip32::ChildNumber::from(13),
spend_txid: None,
spent_at: None,
}]);
let res = control.create_spend(&[dummy_op], &destinations, 1).unwrap();
let tx = res.psbt.global.unsigned_tx;
@ -660,16 +662,20 @@ mod tests {
Coin {
outpoint: dummy_op_a,
block_height: None,
block_time: None,
amount: bitcoin::Amount::from_sat(100_000),
derivation_index: bip32::ChildNumber::from(13),
spend_txid: None,
spent_at: None,
},
Coin {
outpoint: dummy_op_b,
block_height: None,
block_time: None,
amount: bitcoin::Amount::from_sat(115_680),
derivation_index: bip32::ChildNumber::from(34),
spend_txid: None,
spent_at: None,
},
]);

View File

@ -60,12 +60,15 @@ pub trait DatabaseConnection {
/// Store new UTxOs. Coins must not already be in database.
fn new_unspent_coins<'a>(&mut self, coins: &[Coin]);
/// Mark a set of coins as being confirmed at a specified height.
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32)]);
/// Mark a set of coins as being confirmed at a specified height and block time.
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32, u32)]);
/// Mark a set of coins as being spent by a specified txid.
fn spend_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)]);
/// Mark a set of coins as spent by a specified txid at a specified block time.
fn confirm_spend<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, u32)]);
/// Get specific coins from the database.
fn coins_by_outpoints(
&mut self,
@ -84,33 +87,6 @@ pub trait DatabaseConnection {
fn delete_spend(&mut self, txid: &bitcoin::Txid);
}
// FIXME: if possible, avoid reallocating.
fn db_coins_into_coins(db_coins: Vec<DbCoin>) -> HashMap<bitcoin::OutPoint, Coin> {
db_coins
.into_iter()
.map(|db_coin| {
let DbCoin {
outpoint,
block_height,
amount,
derivation_index,
spend_txid,
..
} = db_coin;
(
outpoint,
Coin {
outpoint,
block_height,
amount,
derivation_index,
spend_txid,
},
)
})
.collect()
}
impl DatabaseConnection for SqliteConn {
fn chain_tip(&mut self) -> Option<BlockChainTip> {
match self.db_tip() {
@ -147,7 +123,7 @@ impl DatabaseConnection for SqliteConn {
self.new_unspent_coins(coins)
}
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32)]) {
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32, u32)]) {
self.confirm_coins(outpoints)
}
@ -155,6 +131,10 @@ impl DatabaseConnection for SqliteConn {
self.spend_coins(outpoints)
}
fn confirm_spend<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, u32)]) {
self.confirm_spend(outpoints)
}
fn derivation_index_by_address(
&mut self,
address: &bitcoin::Address,
@ -190,13 +170,46 @@ impl DatabaseConnection for SqliteConn {
}
}
// FIXME: if possible, avoid reallocating.
fn db_coins_into_coins(db_coins: Vec<DbCoin>) -> HashMap<bitcoin::OutPoint, Coin> {
db_coins
.into_iter()
.map(|db_coin| {
let DbCoin {
outpoint,
block_height,
block_time,
amount,
derivation_index,
spend_txid,
spent_at,
..
} = db_coin;
(
outpoint,
Coin {
outpoint,
block_height,
block_time,
amount,
derivation_index,
spend_txid,
spent_at,
},
)
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Coin {
pub outpoint: bitcoin::OutPoint,
pub block_height: Option<i32>,
pub block_time: Option<u32>,
pub amount: bitcoin::Amount,
pub derivation_index: bip32::ChildNumber,
pub spend_txid: Option<bitcoin::Txid>,
pub spent_at: Option<u32>,
}
impl std::hash::Hash for Coin {

View File

@ -290,13 +290,13 @@ impl SqliteConn {
/// Mark a set of coins as confirmed.
pub fn confirm_coins<'a>(
&mut self,
outpoints: impl IntoIterator<Item = &'a (bitcoin::OutPoint, i32)>,
outpoints: impl IntoIterator<Item = &'a (bitcoin::OutPoint, i32, u32)>,
) {
db_exec(&mut self.conn, |db_tx| {
for (outpoint, height) in outpoints {
for (outpoint, height, time) in outpoints {
db_tx.execute(
"UPDATE coins SET blockheight = ?1 WHERE txid = ?2 AND vout = ?3",
rusqlite::params![height, outpoint.txid.to_vec(), outpoint.vout,],
"UPDATE coins SET blockheight = ?1, blocktime = ?2 WHERE txid = ?3 AND vout = ?4",
rusqlite::params![height, time, outpoint.txid.to_vec(), outpoint.vout,],
)?;
}
@ -528,9 +528,11 @@ mod tests {
)
.unwrap(),
block_height: None,
block_time: None,
amount: bitcoin::Amount::from_sat(98765),
derivation_index: bip32::ChildNumber::from_normal_idx(10).unwrap(),
spend_txid: None,
spent_at: None,
};
conn.new_unspent_coins(&[coin_a.clone()]); // On 1.48, arrays aren't IntoIterator
assert_eq!(conn.unspent_coins()[0].outpoint, coin_a.outpoint);
@ -547,9 +549,11 @@ mod tests {
)
.unwrap(),
block_height: None,
block_time: None,
amount: bitcoin::Amount::from_sat(1111),
derivation_index: bip32::ChildNumber::from_normal_idx(103).unwrap(),
spend_txid: None,
spent_at: None,
};
conn.new_unspent_coins(&[coin_b.clone()]);
let outpoints: HashSet<bitcoin::OutPoint> = conn
@ -580,10 +584,13 @@ mod tests {
// Now if we confirm one, it'll be marked as such.
let height = 174500;
conn.confirm_coins(&[(coin_a.outpoint, height)]);
let time = 174500;
conn.confirm_coins(&[(coin_a.outpoint, height, time)]);
let coins = conn.unspent_coins();
assert_eq!(coins[0].block_height, Some(height));
assert_eq!(coins[0].block_time, Some(time));
assert!(coins[1].block_height.is_none());
assert!(coins[1].block_time.is_none());
// Now if we spend one, we'll only get the other one.
conn.spend_coins(&[(

View File

@ -35,11 +35,14 @@ CREATE TABLE coins (
id INTEGER PRIMARY KEY NOT NULL,
wallet_id INTEGER NOT NULL,
blockheight INTEGER,
blocktime INTEGER,
txid BLOB NOT NULL,
vout INTEGER NOT NULL,
amount_sat INTEGER NOT NULL,
derivation_index INTEGER NOT NULL,
spend_txid BLOB,
/* Time of the block containing the transaction spending the coin, NULL if not confirmed */
spent_at INTEGER,
UNIQUE (txid, vout),
FOREIGN KEY (wallet_id) REFERENCES wallets (id)
ON UPDATE RESTRICT
@ -129,9 +132,11 @@ pub struct DbCoin {
pub wallet_id: i64,
pub outpoint: bitcoin::OutPoint,
pub block_height: Option<i32>,
pub block_time: Option<u32>,
pub amount: bitcoin::Amount,
pub derivation_index: bip32::ChildNumber,
pub spend_txid: Option<bitcoin::Txid>,
pub spent_at: Option<u32>,
}
impl std::hash::Hash for DbCoin {
@ -148,28 +153,32 @@ impl TryFrom<&rusqlite::Row<'_>> for DbCoin {
let wallet_id = row.get(1)?;
let block_height = row.get(2)?;
let txid: Vec<u8> = row.get(3)?;
let block_time = row.get(3)?;
let txid: Vec<u8> = row.get(4)?;
let txid: bitcoin::Txid = encode::deserialize(&txid).expect("We only store valid txids");
let vout = row.get(4)?;
let vout = row.get(5)?;
let outpoint = bitcoin::OutPoint { txid, vout };
let amount = row.get(5)?;
let amount = row.get(6)?;
let amount = bitcoin::Amount::from_sat(amount);
let der_idx: u32 = row.get(6)?;
let der_idx: u32 = row.get(7)?;
let derivation_index = bip32::ChildNumber::from(der_idx);
let spend_txid: Option<Vec<u8>> = row.get(7)?;
let spend_txid: Option<Vec<u8>> = row.get(8)?;
let spend_txid =
spend_txid.map(|txid| encode::deserialize(&txid).expect("We only store valid txids"));
let spent_at = row.get(9)?;
Ok(DbCoin {
id,
wallet_id,
outpoint,
block_height,
block_time,
amount,
derivation_index,
spend_txid,
spent_at,
})
}
}

View File

@ -48,11 +48,11 @@ impl BitcoinInterface for DummyBitcoind {
Vec::new()
}
fn confirmed_coins(&self, _: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, i32)> {
fn confirmed_coins(&self, _: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, i32, u32)> {
Vec::new()
}
fn spent_coins(&self, _: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
fn spending_coins(&self, _: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
Vec::new()
}
}
@ -121,21 +121,35 @@ impl DatabaseConnection for DummyDbConn {
}
}
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32)]) {
for (op, height) in outpoints {
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32, u32)]) {
for (op, height, time) in outpoints {
let mut db = self.db.write().unwrap();
let h = &mut db.coins.get_mut(op).unwrap().block_height;
assert!(h.is_none());
*h = Some(*height);
let coin = &mut db.coins.get_mut(op).unwrap();
assert!(coin.block_height.is_none());
assert!(coin.block_time.is_none());
coin.block_height = Some(*height);
coin.block_time = Some(*time);
}
}
fn spend_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)]) {
for (op, spend_txid) in outpoints {
let mut db = self.db.write().unwrap();
let spender = &mut db.coins.get_mut(op).unwrap().spend_txid;
assert!(spender.is_none());
*spender = Some(*spend_txid);
let spent = &mut db.coins.get_mut(op).unwrap();
assert!(spent.spend_txid.is_none());
assert!(spent.spent_at.is_none());
spent.spend_txid = Some(*spend_txid);
}
}
fn confirm_spend<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, u32)]) {
for (op, spend_txid, time) in outpoints {
let mut db = self.db.write().unwrap();
let spent = &mut db.coins.get_mut(op).unwrap();
assert!(!spent.spend_txid.is_none());
assert!(spent.spent_at.is_none());
spent.spend_txid = Some(*spend_txid);
spent.spent_at = Some(*time);
}
}