Repository: wizardsardine/liana (rust)

Issue:
`startrescan` allows timestamps before genesis block on test networks
Entered a 2015 date as rescan date for a signet setup.
Rescan stopped at 100%, no errors handled by the GUI, only an error in logs:
`2024-01-03T15:09:04.881557Z ERROR liana::bitcoin::poller::looper:289: Could not retrieve block height for timestamp '1421020800'`

Pull request description:
Get genesis timestamp from bitcoind
fixes #904 

Patch:
diff --git a/gui/Cargo.lock b/gui/Cargo.lock
index e33c7e61..6ae01d55 100644
--- a/gui/Cargo.lock
+++ b/gui/Cargo.lock
@@ -994,9 +994,9 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35"
 
 [[package]]
 name = "curve25519-dalek"
-version = "4.1.2"
+version = "4.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348"
+checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -5824,9 +5824,9 @@ dependencies = [
 
 [[package]]
 name = "x25519-dalek"
-version = "2.0.1"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
+checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96"
 dependencies = [
  "curve25519-dalek",
  "rand_core",
diff --git a/src/bitcoin/mod.rs b/src/bitcoin/mod.rs
index 60388c1e..914525cf 100644
--- a/src/bitcoin/mod.rs
+++ b/src/bitcoin/mod.rs
@@ -40,6 +40,8 @@ impl fmt::Display for BlockChainTip {
 
 /// Our Bitcoin backend.
 pub trait BitcoinInterface: Send {
+    fn genesis_block_timestamp(&self) -> u32;
+
     fn genesis_block(&self) -> BlockChainTip;
 
     /// Get the progress of the block chain synchronization.
@@ -119,6 +121,15 @@ pub trait BitcoinInterface: Send {
 }
 
 impl BitcoinInterface for d::BitcoinD {
+    fn genesis_block_timestamp(&self) -> u32 {
+        self.get_block_stats(
+            self.get_block_hash(0)
+                .expect("Genesis block hash must always be there"),
+        )
+        .expect("Genesis block must always be there")
+        .time
+    }
+
     fn genesis_block(&self) -> BlockChainTip {
         let height = 0;
         let hash = self
@@ -370,6 +381,10 @@ impl BitcoinInterface for d::BitcoinD {
 
 // FIXME: do we need to repeat the entire trait implemenation? Isn't there a nicer way?
 impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>> {
+    fn genesis_block_timestamp(&self) -> u32 {
+        self.lock().unwrap().genesis_block_timestamp()
+    }
+
     fn genesis_block(&self) -> BlockChainTip {
         self.lock().unwrap().genesis_block()
     }
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index bf473bf6..28a0620e 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -33,9 +33,6 @@ use miniscript::{
 };
 use serde::{Deserialize, Serialize};
 
-// 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 {
     NoOutpointForSelfSend,
@@ -888,13 +885,14 @@ impl DaemonControl {
     /// 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();
+        let genesis_timestamp = self.bitcoin.genesis_block_timestamp();
 
         let future_timestamp = self
             .bitcoin
             .tip_time()
             .map(|t| timestamp >= t)
             .unwrap_or(false);
-        if timestamp < MAINNET_GENESIS_TIME || future_timestamp {
+        if timestamp < genesis_timestamp || future_timestamp {
             return Err(CommandError::InsaneRescanTimestamp(timestamp));
         }
         if db_conn.rescan_timestamp().is_some() || self.bitcoin.rescan_progress().is_some() {


Test patch:
diff --git a/src/testutils.rs b/src/testutils.rs
index 9034aaec..7fe3141d 100644
--- a/src/testutils.rs
+++ b/src/testutils.rs
@@ -34,6 +34,10 @@ impl DummyBitcoind {
 }
 
 impl BitcoinInterface for DummyBitcoind {
+    fn genesis_block_timestamp(&self) -> u32 {
+        1231006505
+    }
+
     fn genesis_block(&self) -> BlockChainTip {
         let hash = bitcoin::BlockHash::from_str(
             "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
diff --git a/tests/test_rpc.py b/tests/test_rpc.py
index 904d17b8..9144ad88 100644
--- a/tests/test_rpc.py
+++ b/tests/test_rpc.py
@@ -22,7 +22,7 @@ from test_framework.utils import (
 
 def test_getinfo(lianad):
     res = lianad.rpc.getinfo()
-    assert 'timestamp' in res.keys()
+    assert "timestamp" in res.keys()
     assert res["version"] == "4.0.0-dev"
     assert res["network"] == "regtest"
     wait_for(lambda: lianad.rpc.getinfo()["block_height"] == 101)
@@ -599,10 +599,15 @@ def test_start_rescan(lianad, bitcoind):
     with pytest.raises(RpcError, match="Insane timestamp.*"):
         lianad.rpc.startrescan(future_timestamp)
     assert lianad.rpc.getinfo()["rescan_progress"] is None
-    prebitcoin_timestamp = 1231006505 - 1
+    block_hash = bitcoind.rpc.getblockhash(0)
+    genesis_timestamp = bitcoind.rpc.getblock(block_hash)["time"]
+    prebitcoin_timestamp = genesis_timestamp - 1
     with pytest.raises(RpcError, match="Insane timestamp."):
         lianad.rpc.startrescan(prebitcoin_timestamp)
     assert lianad.rpc.getinfo()["rescan_progress"] is None
+    # we can rescan from genesis block
+    lianad.rpc.startrescan(genesis_timestamp)
+    wait_for(lambda: lianad.rpc.getinfo()["rescan_progress"] is None)
 
     # First, get some coins
     for _ in range(10):
@@ -1232,9 +1237,7 @@ def test_rbfpsbt_cancel(lianad, bitcoind):
     # But we can't set the feerate explicitly.
     with pytest.raises(
         RpcError,
-        match=re.escape(
-            "A feerate must not be provided if creating a cancel."
-        ),
+        match=re.escape("A feerate must not be provided if creating a cancel."),
     ):
         rbf_1_res = lianad.rpc.rbfpsbt(first_txid, True, 2)
     rbf_1_psbt = PSBT.from_base64(rbf_1_res["psbt"])