diff --git a/doc/API.md b/doc/API.md index 75b18a4a..0ab178fe 100644 --- a/doc/API.md +++ b/doc/API.md @@ -13,6 +13,7 @@ Commands must be sent as valid JSONRPC 2.0 requests, ending with a `\n`. | [`listspendtxs`](#listspendtxs) | List all stored Spend transactions | | [`delspendtx`](#delspendtx) | Delete a stored Spend transaction | | [`broadcastspend`](#broadcastspend) | Finalize a stored Spend PSBT, and broadcast it | +| [`startrescan`](#startrescan) | Start rescanning the block chain from a given date | # Reference @@ -199,3 +200,19 @@ This command does not return anything for now. | Field | Type | Description | | -------------- | --------- | ---------------------------------------------------- | + + +### `startrescan` + +#### Request + +| Field | Type | Description | +| ------------ | ------ | ------------------------------------------------------ | +| `timestamp` | int | Date to start rescanning from, as a UNIX timestamp | + +#### Response + +This command does not return anything for now. + +| Field | Type | Description | +| -------------- | --------- | ---------------------------------------------------- | diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 4c189e23..38b93b96 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -38,6 +38,9 @@ const MAX_FEE: u64 = bitcoin::blockdata::constants::COIN_VALUE; // Assume that paying more than 1000sat/vb in feerate is a bug. const MAX_FEERATE: u64 = bitcoin::blockdata::constants::COIN_VALUE; +// Timestamp in the header of the genesis block. Used for sanity checks. +const MAINNET_GENESIS_TIME: u32 = 1231006505; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum CommandError { NoOutpoint, @@ -56,6 +59,8 @@ pub enum CommandError { // FIXME: when upgrading Miniscript put the actual error there SpendFinalization(String), TxBroadcast(String), + AlreadyRescanning, + InsaneRescanTimestamp(u32), } impl fmt::Display for CommandError { @@ -82,6 +87,11 @@ impl fmt::Display for CommandError { write!(f, "Failed to finalize the spend transaction PSBT: '{}'.", e) } Self::TxBroadcast(e) => write!(f, "Failed to broadcast transaction: '{}'.", e), + Self::AlreadyRescanning => write!( + f, + "There is already a rescan ongoing. Please wait for it to complete first." + ), + Self::InsaneRescanTimestamp(t) => write!(f, "Insane timestamp '{}'.", t), } } } @@ -490,6 +500,26 @@ impl DaemonControl { Err(BitcoinError::Broadcast(e)) => Err(CommandError::TxBroadcast(e)), } } + + /// Trigger a rescan of the block chain for transactions involving our main descriptor between + /// the given date and the current tip. + /// The date must be after the genesis block time and before the current tip blocktime. + pub fn start_rescan(&self, timestamp: u32) -> Result<(), CommandError> { + let mut db_conn = self.db.connection(); + + if db_conn.rescan_timestamp().is_some() { + return Err(CommandError::AlreadyRescanning); + } + if timestamp < MAINNET_GENESIS_TIME || timestamp >= self.bitcoin.tip_time() { + return Err(CommandError::InsaneRescanTimestamp(timestamp)); + } + + self.bitcoin + .start_rescan(&self.config.main_descriptor, timestamp); + db_conn.set_rescan(timestamp); + + Ok(()) + } } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/jsonrpc/api.rs b/src/jsonrpc/api.rs index 0d034643..6876ab8d 100644 --- a/src/jsonrpc/api.rs +++ b/src/jsonrpc/api.rs @@ -84,6 +84,18 @@ fn broadcast_spend(control: &DaemonControl, params: Params) -> Result Result { + let timestamp: u32 = params + .get(0, "timestamp") + .ok_or_else(|| Error::invalid_params("Missing 'timestamp' parameter."))? + .as_u64() + .and_then(|t| t.try_into().ok()) + .ok_or_else(|| Error::invalid_params("Invalid 'timestamp' parameter."))?; + control.start_rescan(timestamp)?; + + Ok(serde_json::json!({})) +} + /// Handle an incoming JSONRPC2 request. pub fn handle_request(control: &DaemonControl, req: Request) -> Result { let result = match req.method.as_str() { @@ -111,6 +123,12 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result serde_json::json!(&control.get_new_address()), "listcoins" => serde_json::json!(&control.list_coins()), "listspendtxs" => serde_json::json!(&control.list_spend()), + "startrescan" => { + let params = req + .params + .ok_or_else(|| Error::invalid_params("Missing 'timestamp' parameter."))?; + start_rescan(control, params)? + } "stop" => serde_json::json!({}), "updatespend" => { let params = req diff --git a/src/jsonrpc/mod.rs b/src/jsonrpc/mod.rs index db721891..595dce33 100644 --- a/src/jsonrpc/mod.rs +++ b/src/jsonrpc/mod.rs @@ -159,7 +159,9 @@ impl From for Error { | commands::CommandError::InvalidOutputValue(..) | commands::CommandError::InsufficientFunds(..) | commands::CommandError::UnknownSpend(..) - | commands::CommandError::SpendFinalization(..) => { + | commands::CommandError::SpendFinalization(..) + | commands::CommandError::InsaneRescanTimestamp(..) + | commands::CommandError::AlreadyRescanning => { Error::new(ErrorCode::InvalidParams, e.to_string()) } commands::CommandError::SanityCheckFailure(_) => {