Skip to content

Commit

Permalink
Merge #1376: Get timestamp of last completed poll of the blockchain
Browse files Browse the repository at this point in the history
dee9554 database: add `wallet()` method and use in `getinfo` command (Michael Mallan)
4694eaa poller: don't poll now if blockchain syncing (Michael Mallan)
0f9f1f3 commands: return last poll timestamp from `getinfo` (Michael Mallan)
c6add0a qa: add method to get lianad poll interval (Michael Mallan)
61e39f7 poller: store last poll timestamp (Michael Mallan)
e9fdcde database: get and set last poll timestamp (Michael Mallan)
a2b79f1 sqlite: get and set last poll timestamp (Michael Mallan)

Pull request description:

  This is a first step towards #1373.

  The timestamp of the last completed poll of the blockchain will be stored in the database, and this value will be made available via the `getinfo` command.

ACKs for top commit:
  edouardparis:
    ACK dee9554

Tree-SHA512: 32706d516e9915d7320583b37990a5d907bb254905efbb0318c5eae4b27753800933d1b9e68bf43a0190c27d8a5d349f633c0e6e70ba21815ed6eed1648f0625
  • Loading branch information
edouardparis committed Oct 24, 2024
2 parents c23320a + dee9554 commit c2ce025
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 30 deletions.
19 changes: 10 additions & 9 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,16 @@ This command does not take any parameter for now.

#### Response

| Field | Type | Description |
| -------------------- | ------------- | -------------------------------------------------------------------------------------------- |
| `version` | string | Version following the [SimVer](http://www.simver.org/) format |
| `network` | string | Answer can be `mainnet`, `testnet`, `regtest` |
| `block_height` | integer | The block height we are synced at. |
| `sync` | float | The synchronization progress as percentage (`0 < sync < 1`) |
| `descriptors` | object | Object with the name of the descriptor as key and the descriptor string as value |
| `rescan_progress` | float or null | Progress of an ongoing rescan as a percentage (between 0 and 1) if there is any |
| `timestamp` | integer | Unix timestamp of wallet creation date |
| Field | Type | Description |
| -------------------- | --------------- | -------------------------------------------------------------------------------------------- |
| `version` | string | Version following the [SimVer](http://www.simver.org/) format |
| `network` | string | Answer can be `mainnet`, `testnet`, `regtest` |
| `block_height` | integer | The block height we are synced at. |
| `sync` | float | The synchronization progress as percentage (`0 < sync < 1`) |
| `descriptors` | object | Object with the name of the descriptor as key and the descriptor string as value |
| `rescan_progress` | float or null | Progress of an ongoing rescan as a percentage (between 0 and 1) if there is any |
| `timestamp` | integer | Unix timestamp of wallet creation date |
| `last_poll_timestamp`| integer or null | Unix timestamp of last poll (if any) of the blockchain |

### `getnewaddress`

Expand Down
9 changes: 8 additions & 1 deletion src/bitcoin/poller/looper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
descriptors,
};

use std::{collections::HashSet, sync, thread, time};
use std::{collections::HashSet, convert::TryInto, sync, thread, time};

use miniscript::bitcoin::{self, secp256k1};

Expand Down Expand Up @@ -401,4 +401,11 @@ pub fn poll(
let mut db_conn = db.connection();
updates(&mut db_conn, bit, descs, secp);
rescan_check(&mut db_conn, bit, descs, secp);
let now: u32 = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)
.expect("current system time must be later than epoch")
.as_secs()
.try_into()
.expect("system clock year is earlier than 2106");
db_conn.set_last_poll(now);
}
23 changes: 21 additions & 2 deletions src/bitcoin/poller/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,28 @@ impl Poller {
}
Ok(PollerMessage::PollNow(sender)) => {
// We've been asked to poll, don't wait any further and signal completion to
// the caller.
// the caller, unless the block chain is still syncing.
// Polling while the block chain is syncing could lead to poller restarts
// if the height increases before completion, and in any case this is consistent
// with regular poller behaviour.
if !synced {
let progress = self.bit.sync_progress();
log::info!(
"Block chain synchronization progress: {:.2}% ({} blocks / {} headers)",
progress.rounded_up_progress() * 100.0,
progress.blocks,
progress.headers
);
synced = progress.is_complete();
}
// Update `last_poll` even if we don't poll now so that we don't attempt another
// poll too soon.
last_poll = Some(time::Instant::now());
looper::poll(&mut self.bit, &self.db, &self.secp, &self.descs);
if synced {
looper::poll(&mut self.bit, &self.db, &self.secp, &self.descs);
} else {
log::warn!("Skipped poll as block chain is still synchronizing.");
}
if let Err(e) = sender.send(()) {
log::error!("Error sending immediate poll completion signal: {}.", e);
}
Expand Down
11 changes: 7 additions & 4 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,10 @@ impl DaemonControl {
/// Get information about the current state of the daemon
pub fn get_info(&self) -> GetInfoResult {
let mut db_conn = self.db.connection();

let block_height = db_conn.chain_tip().map(|tip| tip.height).unwrap_or(0);
let rescan_progress = db_conn
.rescan_timestamp()
let wallet = db_conn.wallet();
let rescan_progress = wallet
.rescan_timestamp
.map(|_| self.bitcoin.rescan_progress().unwrap_or(1.0));
GetInfoResult {
version: VERSION.to_string(),
Expand All @@ -323,7 +323,8 @@ impl DaemonControl {
main: self.config.main_descriptor.clone(),
},
rescan_progress,
timestamp: db_conn.timestamp(),
timestamp: wallet.timestamp,
last_poll_timestamp: wallet.last_poll_timestamp,
}
}

Expand Down Expand Up @@ -1127,6 +1128,8 @@ pub struct GetInfoResult {
pub rescan_progress: Option<f64>,
/// Timestamp at wallet creation date
pub timestamp: u32,
/// Timestamp of last poll, if any.
pub last_poll_timestamp: Option<u32>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
56 changes: 52 additions & 4 deletions src/database/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@ use std::{

use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1};

/// Information about the wallet.
///
/// All timestamps are the number of seconds since the UNIX epoch.
#[derive(Clone, Debug)]
pub struct Wallet {
/// Timestamp at wallet creation time.
pub timestamp: u32,
/// Derivation index for the next receiving address.
pub receive_index: bip32::ChildNumber,
/// Derivation index for the next change address.
pub change_index: bip32::ChildNumber,
/// Timestamp to start rescanning from, if any.
pub rescan_timestamp: Option<u32>,
/// Timestamp at which the last poll of the blockchain completed, if any,
pub last_poll_timestamp: Option<u32>,
}

pub trait DatabaseInterface: Send {
fn connection(&self) -> Box<dyn DatabaseConnection>;
}
Expand All @@ -46,6 +63,9 @@ pub trait DatabaseConnection {
/// The network we are operating on.
fn network(&mut self) -> bitcoin::Network;

/// Get the `Wallet`.
fn wallet(&mut self) -> Wallet;

/// The timestamp at wallet creation time
fn timestamp(&mut self) -> u32;

Expand Down Expand Up @@ -81,6 +101,14 @@ pub trait DatabaseConnection {
/// Mark the rescan as complete.
fn complete_rescan(&mut self);

/// Get the timestamp at which the last poll of the blockchain completed, if any,
/// as the number of seconds since the UNIX epoch.
fn last_poll_timestamp(&mut self) -> Option<u32>;

/// Set the timestamp at which the last poll of the blockchain completed,
/// where `timestamp` should be given as the number of seconds since the UNIX epoch.
fn set_last_poll(&mut self, timestamp: u32);

/// Get the derivation index for this address, as well as whether this address is change.
fn derivation_index_by_address(
&mut self,
Expand Down Expand Up @@ -176,16 +204,27 @@ impl DatabaseConnection for SqliteConn {
self.db_tip().network
}

fn wallet(&mut self) -> Wallet {
let db_wallet = self.db_wallet();
Wallet {
timestamp: db_wallet.timestamp,
receive_index: db_wallet.deposit_derivation_index,
change_index: db_wallet.change_derivation_index,
rescan_timestamp: db_wallet.rescan_timestamp,
last_poll_timestamp: db_wallet.last_poll_timestamp,
}
}

fn timestamp(&mut self) -> u32 {
self.db_wallet().timestamp
self.wallet().timestamp
}

fn update_tip(&mut self, tip: &BlockChainTip) {
self.update_tip(tip)
}

fn receive_index(&mut self) -> bip32::ChildNumber {
self.db_wallet().deposit_derivation_index
self.wallet().receive_index
}

fn set_receive_index(
Expand All @@ -197,7 +236,7 @@ impl DatabaseConnection for SqliteConn {
}

fn change_index(&mut self) -> bip32::ChildNumber {
self.db_wallet().change_derivation_index
self.wallet().change_index
}

fn set_change_index(
Expand All @@ -209,7 +248,7 @@ impl DatabaseConnection for SqliteConn {
}

fn rescan_timestamp(&mut self) -> Option<u32> {
self.db_wallet().rescan_timestamp
self.wallet().rescan_timestamp
}

fn set_rescan(&mut self, timestamp: u32) {
Expand All @@ -220,6 +259,15 @@ impl DatabaseConnection for SqliteConn {
self.complete_wallet_rescan()
}

fn last_poll_timestamp(&mut self) -> Option<u32> {
self.wallet().last_poll_timestamp
}

fn set_last_poll(&mut self, timestamp: u32) {
self.set_wallet_last_poll_timestamp(timestamp)
.expect("database must be available")
}

fn coins(
&mut self,
statuses: &[CoinStatus],
Expand Down
34 changes: 28 additions & 6 deletions src/database/sqlite/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ use miniscript::bitcoin::{
secp256k1,
};

const DB_VERSION: i64 = 5;
const DB_VERSION: i64 = 6;

/// 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.
Expand Down Expand Up @@ -371,6 +371,20 @@ impl SqliteConn {
.expect("Database must be available");
}

// Sqlite supports i64 integers so we use u32 for the timestamp.
/// Set the last poll timestamp, where `timestamp` is seconds since UNIX epoch.
pub fn set_wallet_last_poll_timestamp(&mut self, timestamp: u32) -> Result<(), SqliteDbError> {
db_exec(&mut self.conn, |db_tx| {
db_tx
.execute(
"UPDATE wallets SET last_poll_timestamp = (?1) WHERE id = (?2)",
rusqlite::params![timestamp, WALLET_ID],
)
.map(|_| ())
})
.map_err(SqliteDbError::Rusqlite)
}

/// Get all the coins from DB, optionally filtered by coin status and/or outpoint.
pub fn coins(
&mut self,
Expand Down Expand Up @@ -2384,7 +2398,7 @@ CREATE TABLE labels (
}

#[test]
fn v0_to_v5_migration() {
fn v0_to_v6_migration() {
let secp = secp256k1::Secp256k1::verification_only();

// Create a database with version 0, using the old schema.
Expand Down Expand Up @@ -2490,7 +2504,7 @@ CREATE TABLE labels (
{
let mut conn = db.connection().unwrap();
let version = conn.db_version();
assert_eq!(version, 5);
assert_eq!(version, 6);
}
// We should now be able to insert another PSBT, to query both, and the first PSBT must
// have no associated timestamp.
Expand Down Expand Up @@ -2552,11 +2566,19 @@ CREATE TABLE labels (
assert_eq!(db_labels[0].value, "hello");
}

// In v6, we can get and set the last poll timestamp.
{
let mut conn = db.connection().unwrap();
assert!(conn.db_wallet().last_poll_timestamp.is_none());
conn.set_wallet_last_poll_timestamp(1234567).unwrap();
assert_eq!(conn.db_wallet().last_poll_timestamp, Some(1234567));
}

fs::remove_dir_all(tmp_dir).unwrap();
}

#[test]
fn v3_to_v5_migration() {
fn v3_to_v6_migration() {
let secp = secp256k1::Secp256k1::verification_only();

// Create a database with version 3, using the old schema.
Expand Down Expand Up @@ -2718,10 +2740,10 @@ CREATE TABLE labels (

// Migrate the DB.
maybe_apply_migration(&db_path, &bitcoin_txs).unwrap();
assert_eq!(conn.db_version(), 5);
assert_eq!(conn.db_version(), 6);
// 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() == 5);
assert!(conn.db_version() == 6);
let coins_post = conn.coins(&[], &[]);
assert_eq!(coins_pre, coins_post);
}
Expand Down
6 changes: 5 additions & 1 deletion src/database/sqlite/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ CREATE TABLE wallets (
main_descriptor TEXT NOT NULL,
deposit_derivation_index INTEGER NOT NULL,
change_derivation_index INTEGER NOT NULL,
rescan_timestamp INTEGER
rescan_timestamp INTEGER,
last_poll_timestamp INTEGER
);
/* Our (U)TxOs.
Expand Down Expand Up @@ -140,6 +141,7 @@ pub struct DbWallet {
pub deposit_derivation_index: bip32::ChildNumber,
pub change_derivation_index: bip32::ChildNumber,
pub rescan_timestamp: Option<u32>,
pub last_poll_timestamp: Option<u32>,
}

impl TryFrom<&rusqlite::Row<'_>> for DbWallet {
Expand All @@ -159,6 +161,7 @@ impl TryFrom<&rusqlite::Row<'_>> for DbWallet {
let change_derivation_index = bip32::ChildNumber::from(der_idx);

let rescan_timestamp = row.get(5)?;
let last_poll_timestamp = row.get(6)?;

Ok(DbWallet {
id,
Expand All @@ -167,6 +170,7 @@ impl TryFrom<&rusqlite::Row<'_>> for DbWallet {
deposit_derivation_index,
change_derivation_index,
rescan_timestamp,
last_poll_timestamp,
})
}
}
Expand Down
18 changes: 18 additions & 0 deletions src/database/sqlite/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,19 @@ fn migrate_v4_to_v5(
Ok(())
}

fn migrate_v5_to_v6(conn: &mut rusqlite::Connection) -> Result<(), SqliteDbError> {
db_exec(conn, |tx| {
tx.execute(
"ALTER TABLE wallets ADD COLUMN last_poll_timestamp INTEGER",
rusqlite::params![],
)?;
tx.execute("UPDATE version SET version = 6", rusqlite::params![])?;
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
Expand Down Expand Up @@ -342,6 +355,11 @@ pub fn maybe_apply_migration(
migrate_v4_to_v5(&mut conn, bitcoin_txs)?;
log::warn!("Migration from database version 4 to version 5 successful.");
}
5 => {
log::warn!("Upgrading database from version 5 to version 6.");
migrate_v5_to_v6(&mut conn)?;
log::warn!("Migration from database version 5 to version 6 successful.");
}
_ => return Err(SqliteDbError::UnsupportedVersion(version)),
}
}
Expand Down
Loading

0 comments on commit c2ce025

Please sign in to comment.