Repository: wizardsardine/liana
Pull request #965 (2024-02-08 14:41:55)

poller: unspend expired before new spend
This change ensures that the spend txid of a coin is updated directly to another value in case a conflicting spend is detected.

The previous order caused the spend txid to first be cleared, which would misleadingly make the coin appear as confirmed
rather than spending.

I've added a new utils function for the functional tests that is a slight generalisation of `wait_for` with an additional condition that must always be met while waiting.

`wait_for` now calls this new function with the condition being one that is always true.

jp1ac4: Refactored the new utils function.
jp1ac4: Rebased on master.
darosior: In a followup it would be worthwile to introduce this new `wait_for_while_condition_holds` helper for the rescan tests.

Patch:
diff --git a/doc/API.md b/doc/API.md
index 68b0094b..e85b4f0c 100644
--- a/doc/API.md
+++ b/doc/API.md
@@ -149,8 +149,7 @@ A coin may have one of the following four statuses:
 Create a transaction spending one or more of our coins. All coins must exist and not be spent.
 
 If no coins are specified in `outpoints`, they will be selected automatically from the set of
-confirmed coins together with any unconfirmed coins that are change outputs
-(see [`listcoins`](#listcoins) for coin status definitions).
+confirmed coins (see [`listcoins`](#listcoins) for coin status definitions).
 
 Will error if the given coins are not sufficient to cover the transaction cost at 90% (or more) of
 the given feerate. If on the contrary the transaction is more than sufficiently funded, it will
@@ -219,14 +218,12 @@ This command does not return anything for now.
 
 List stored Spend transactions.
 
-
-If `txids` is specified, only list transactions whose `txid` is in `txids`(empty list of `txids` is not allowed).
-
 #### Request
 
-| Field         | Type                       | Description                          |
-| ------------- | -------------------------- | ------------------------------------ |
-| `txids`       | array of string (optional) | Ids of the transactions to retrieve  |
+This command does not take any parameter for now.
+
+| Field         | Type              | Description                                                 |
+| ------------- | ----------------- | ----------------------------------------------------------- |
 
 #### Response
 
diff --git a/gui/Cargo.lock b/gui/Cargo.lock
index ed047fa3..e33c7e61 100644
--- a/gui/Cargo.lock
+++ b/gui/Cargo.lock
@@ -2668,7 +2668,7 @@ dependencies = [
 [[package]]
 name = "liana"
 version = "4.0.0"
-source = "git+https://github.com/wizardsardine/liana?branch=master#16afa3e9925cd016db03dbe954403bfa348b89e7"
+source = "git+https://github.com/wizardsardine/liana?branch=master#5a56fdb108351d0a6877a11a1dec7a27a3b0928f"
 dependencies = [
  "backtrace",
  "bdk_coin_select",
@@ -2692,7 +2692,6 @@ version = "4.0.0"
 dependencies = [
  "async-hwi",
  "backtrace",
- "base64 0.21.6",
  "bitcoin_hashes 0.12.0",
  "chrono",
  "dirs 3.0.2",
diff --git a/gui/Cargo.toml b/gui/Cargo.toml
index 7278e79d..09d471c1 100644
--- a/gui/Cargo.toml
+++ b/gui/Cargo.toml
@@ -42,7 +42,6 @@ toml = "0.5"
 chrono = "0.4"
 
 # Used for managing internal bitcoind
-base64 = "0.21"
 bitcoin_hashes = "0.12"
 reqwest = { version = "0.11", default-features=false, features = ["rustls-tls"] }
 rust-ini = "0.19.0"
diff --git a/gui/src/bitcoind.rs b/gui/src/bitcoind.rs
index 73499cd9..69aaa5dc 100644
--- a/gui/src/bitcoind.rs
+++ b/gui/src/bitcoind.rs
@@ -1,12 +1,8 @@
-use base64::Engine;
-use bitcoin_hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
 use liana::{
-    config::BitcoindConfig,
+    config::{BitcoindConfig, BitcoindRpcAuth},
     miniscript::bitcoin::{self, Network},
-    random::{random_bytes, RandomnessError},
 };
 use liana_ui::component::form;
-use std::collections::BTreeMap;
 use std::fmt;
 use std::path::{Path, PathBuf};
 use std::sync::Arc;
@@ -124,243 +120,16 @@ pub fn bitcoind_network_dir(network: &Network) -> Option<String> {
     Some(dir.to_string())
 }
 
-#[derive(PartialEq, Eq, Debug, Clone)]
-pub enum RpcAuthParseError {
-    MissingColon,
-    MissingDollarSign,
-}
-
-impl std::fmt::Display for RpcAuthParseError {
-    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-        match self {
-            Self::MissingColon => write!(
-                f,
-                "RPC auth string should contain colon between user and salt."
-            ),
-            Self::MissingDollarSign => write!(
-                f,
-                "RPC auth string should contain dollar sign between salt and password HMAC."
-            ),
-        }
-    }
-}
-
-/// Represents RPC auth credentials as stored in bitcoin.conf.
-#[derive(PartialEq, Eq, Debug, Clone)]
-pub struct RpcAuth {
-    pub user: String,
-    salt: String,
-    password_hmac: String,
-}
-
-impl RpcAuth {
-    /// Returns a new `RpcAuth` object for the given `user` with a random salt and password.
-    /// This random password is also returned.
-    pub fn new(user: &str) -> Result<(Self, String), RandomnessError> {
-        // RPC auth generation follows approach in
-        // https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py
-        let password =
-            random_bytes().map(|bytes| base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(bytes))?;
-        // As per the Python script, only use 16 bytes for the salt.
-        let salt = random_bytes().map(|bytes| hex::encode(&bytes[..16]))?;
-        let mut engine = HmacEngine::<sha256::Hash>::new(salt.as_bytes());
-        engine.input(password.as_bytes());
-        let password_hmac = Hmac::<sha256::Hash>::from_engine(engine);
-
-        Ok((
-            Self {
-                user: user.to_string(),
-                salt,
-                password_hmac: hex::encode(&password_hmac[..]),
-            },
-            password,
-        ))
-    }
-}
-
-impl std::fmt::Display for RpcAuth {
-    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-        write!(f, "{}:{}${}", self.user, self.salt, self.password_hmac)
-    }
-}
-
-impl std::str::FromStr for RpcAuth {
-    type Err = RpcAuthParseError;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let (user, salt_pw) = s.split_once(':').ok_or(RpcAuthParseError::MissingColon)?;
-        let (salt, pw) = salt_pw
-            .split_once('$')
-            .ok_or(RpcAuthParseError::MissingDollarSign)?;
-        Ok(Self {
-            user: user.to_string(),
-            salt: salt.to_string(),
-            password_hmac: pw.to_string(),
-        })
-    }
-}
-
-/// Represents section for a single network in `bitcoin.conf` file.
-#[derive(PartialEq, Eq, Debug, Clone)]
-pub struct InternalBitcoindNetworkConfig {
-    pub rpc_port: u16,
-    pub p2p_port: u16,
-    pub prune: u32,
-    pub rpc_auth: Option<RpcAuth>,
-}
-
-/// Represents the `bitcoin.conf` file to be used by internal bitcoind.
-#[derive(Debug, Clone)]
-pub struct InternalBitcoindConfig {
-    pub networks: BTreeMap<Network, InternalBitcoindNetworkConfig>,
-}
-
-#[derive(PartialEq, Eq, Debug, Clone)]
-pub enum InternalBitcoindConfigError {
-    KeyNotFound(String),
-    CouldNotParseValue(String),
-    UnexpectedSection(String),
-    TooManyElements(String),
-    FileNotFound,
-    ReadingFile(String),
-    WritingFile(String),
-    Unexpected(String),
-}
-
-impl std::fmt::Display for InternalBitcoindConfigError {
-    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
-        match self {
-            Self::KeyNotFound(e) => write!(f, "Config file does not contain expected key: {}", e),
-            Self::CouldNotParseValue(e) => write!(f, "Value could not be parsed: {}", e),
-            Self::UnexpectedSection(e) => write!(f, "Unexpected section in file: {}", e),
-            Self::TooManyElements(section) => {
-                write!(f, "Section in file contains too many elements: {}", section)
-            }
-            Self::FileNotFound => write!(f, "File not found"),
-            Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e),
-            Self::WritingFile(e) => write!(f, "Error while writing file: {}", e),
-            Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
-        }
-    }
-}
-
-impl Default for InternalBitcoindConfig {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-impl InternalBitcoindConfig {
-    pub fn new() -> Self {
-        Self {
-            networks: BTreeMap::new(),
-        }
-    }
-
-    pub fn from_ini(ini: &ini::Ini) -> Result<Self, InternalBitcoindConfigError> {
-        let mut networks = BTreeMap::new();
-        for (maybe_sec, prop) in ini {
-            if let Some(sec) = maybe_sec {
-                let network = Network::from_core_arg(sec)
-                    .map_err(|e| InternalBitcoindConfigError::UnexpectedSection(e.to_string()))?;
-                if prop.len() > 4 {
-                    return Err(InternalBitcoindConfigError::TooManyElements(
-                        sec.to_string(),
-                    ));
-                }
-                let rpc_port = prop
-                    .get("rpcport")
-                    .ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("rpcport".to_string()))?
-                    .parse::<u16>()
-                    .map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
-                let p2p_port = prop
-                    .get("port")
-                    .ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("port".to_string()))?
-                    .parse::<u16>()
-                    .map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
-                let prune = prop
-                    .get("prune")
-                    .ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("prune".to_string()))?
-                    .parse::<u32>()
-                    .map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
-                let rpc_auth = prop
-                    .get("rpcauth")
-                    .map(|v| {
-                        v.parse::<RpcAuth>().map_err(|e| {
-                            InternalBitcoindConfigError::CouldNotParseValue(e.to_string())
-                        })
-                    })
-                    .transpose()?;
-
-                networks.insert(
-                    network,
-                    InternalBitcoindNetworkConfig {
-                        rpc_port,
-                        p2p_port,
-                        prune,
-                        rpc_auth,
-                    },
-                );
-            } else if !prop.is_empty() {
-                return Err(InternalBitcoindConfigError::UnexpectedSection(
-                    "General section should be empty".to_string(),
-                ));
-            }
-        }
-        Ok(Self { networks })
-    }
-
-    pub fn from_file(path: &PathBuf) -> Result<Self, InternalBitcoindConfigError> {
-        if !path.exists() {
-            return Err(InternalBitcoindConfigError::FileNotFound);
-        }
-        let conf_ini = ini::Ini::load_from_file(path)
-            .map_err(|e| InternalBitcoindConfigError::ReadingFile(e.to_string()))?;
-
-        Self::from_ini(&conf_ini)
-    }
-
-    pub fn to_ini(&self) -> ini::Ini {
-        let mut conf_ini = ini::Ini::new();
-
-        for (network, network_conf) in &self.networks {
-            conf_ini
-                .with_section(Some(network.to_core_arg()))
-                .set("rpcport", network_conf.rpc_port.to_string())
-                .set("port", network_conf.p2p_port.to_string())
-                .set("prune", network_conf.prune.to_string());
-            if let Some(rpc_auth) = network_conf.rpc_auth.as_ref() {
-                conf_ini
-                    .with_section(Some(network.to_core_arg()))
-                    .set("rpcauth", rpc_auth.to_string());
-            }
-        }
-        conf_ini
-    }
-
-    pub fn to_file(&self, path: &PathBuf) -> Result<(), InternalBitcoindConfigError> {
-        std::fs::create_dir_all(
-            path.parent()
-                .ok_or_else(|| InternalBitcoindConfigError::Unexpected("No parent".to_string()))?,
-        )
-        .map_err(|e| InternalBitcoindConfigError::Unexpected(e.to_string()))?;
-        info!("Writing to file {}", path.to_string_lossy());
-        self.to_ini()
-            .write_to_file(path)
-            .map_err(|e| InternalBitcoindConfigError::WritingFile(e.to_string()))?;
-
-        Ok(())
-    }
-}
-
 /// Possible errors when starting bitcoind.
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub enum StartInternalBitcoindError {
     CommandError(String),
+    CouldNotCanonicalizeExePath(String),
     CouldNotCanonicalizeDataDir(String),
+    CouldNotCanonicalizeCookiePath(String),
+    CookieFileNotFound(String),
     BitcoinDError(String),
     ExecutableNotFound,
-    ProcessExited(std::process::ExitStatus),
 }
 
 impl std::fmt::Display for StartInternalBitcoindError {
@@ -369,14 +138,24 @@ impl std::fmt::Display for StartInternalBitcoindError {
             Self::CommandError(e) => {
                 write!(f, "Command to start bitcoind returned an error: {}", e)
             }
+            Self::CouldNotCanonicalizeExePath(e) => {
+                write!(f, "Failed to canonicalize executable path: {}", e)
+            }
             Self::CouldNotCanonicalizeDataDir(e) => {
                 write!(f, "Failed to canonicalize datadir: {}", e)
             }
+            Self::CouldNotCanonicalizeCookiePath(e) => {
+                write!(f, "Failed to canonicalize cookie path: {}", e)
+            }
+            Self::CookieFileNotFound(path) => {
+                write!(
+                    f,
+                    "Cookie file was not found at the expected path: {}",
+                    path
+                )
+            }
             Self::BitcoinDError(e) => write!(f, "bitcoind connection check failed: {}", e),
             Self::ExecutableNotFound => write!(f, "bitcoind executable not found."),
-            Self::ProcessExited(status) => {
-                write!(f, "bitcoind process exited with status '{}'.", status)
-            }
         }
     }
 }
@@ -390,7 +169,7 @@ impl Bitcoind {
     /// Start internal bitcoind for the given network.
     pub fn start(
         network: &bitcoin::Network,
-        config: BitcoindConfig,
+        mut config: BitcoindConfig,
         liana_datadir: &PathBuf,
     ) -> Result<Self, StartInternalBitcoindError> {
         let bitcoind_datadir = internal_bitcoind_datadir(liana_datadir);
@@ -443,43 +222,39 @@ impl Bitcoind {
             .map_err(|e| StartInternalBitcoindError::CommandError(e.to_string()))?;
 
         // We've started bitcoind in the background, however it may fail to start for whatever
-        // reason. And we need its JSONRPC interface to be available to continue. Thus wait for
-        // the interface to be created successfully, regularly checking it did not fail to start.
+        // reason. And we need its JSONRPC interface to be available to continue. Thus wait for it
+        // to have created the cookie file, regularly checking it did not fail to start.
+        let cookie_path = internal_bitcoind_cookie_path(&bitcoind_datadir, network);
         loop {
             match process.try_wait() {
                 Ok(None) => {}
                 Err(e) => log::error!("Error while trying to wait for bitcoind: {}", e),
                 Ok(Some(status)) => {
                     log::error!("Bitcoind exited with status '{}'", status);
-                    return Err(StartInternalBitcoindError::ProcessExited(status));
+                    return Err(StartInternalBitcoindError::CookieFileNotFound(
+                        cookie_path.to_string_lossy().into_owned(),
+                    ));
                 }
             }
-            match liana::BitcoinD::new(&config, "internal_bitcoind_start".to_string()) {
-                Ok(_) => {
-                    log::info!("Bitcoind seems to have successfully started.");
-                    return Ok(Self {
-                        config,
-                        _process: Arc::new(process),
-                    });
-                }
-                Err(liana::BitcoindError::CookieFile(_)) => {
-                    // This is only raised if we're using cookie authentication.
-                    // Assume cookie file has not been created yet and try again.
-                }
-                Err(e) => {
-                    if !e.is_transient() {
-                        // Non-transient error could happen, e.g., if RPC auth credentials are wrong.
-                        // Kill process now in case it's not possible to do via RPC command later.
-                        if let Err(e) = process.kill() {
-                            log::error!("Error trying to kill bitcoind process: '{}'", e);
-                        }
-                        return Err(StartInternalBitcoindError::BitcoinDError(e.to_string()));
-                    }
-                }
+            if cookie_path.exists() {
+                log::info!("Bitcoind seems to have successfully started.");
+                break;
             }
             log::info!("Waiting for bitcoind to start.");
             thread::sleep(time::Duration::from_millis(500));
         }
+
+        config.rpc_auth = BitcoindRpcAuth::CookieFile(cookie_path.canonicalize().map_err(|e| {
+            StartInternalBitcoindError::CouldNotCanonicalizeCookiePath(e.to_string())
+        })?);
+
+        liana::BitcoinD::new(&config, "internal_bitcoind_start".to_string())
+            .map_err(|e| StartInternalBitcoindError::BitcoinDError(e.to_string()))?;
+
+        Ok(Self {
+            config,
+            _process: Arc::new(process),
+        })
     }
 
     /// Stop (internal) bitcoind.
@@ -543,88 +318,3 @@ impl fmt::Display for ConfigField {
         }
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use ini::Ini;
-    use liana::miniscript::bitcoin::Network;
-
-    // Test the format of the internal bitcoind configuration file.
-    #[test]
-    fn internal_bitcoind_config() {
-        // A valid config
-        let mut conf_ini = Ini::new();
-        conf_ini
-            .with_section(Some("main"))
-            .set("rpcport", "43345")
-            .set("port", "42355")
-            .set("prune", "15246");
-        conf_ini
-            .with_section(Some("regtest"))
-            .set("rpcport", "34067")
-            .set("port", "45175")
-            .set("prune", "2043")
-            .set("rpcauth", "my_user:my_salt$my_pw_hmac");
-        let conf = InternalBitcoindConfig::from_ini(&conf_ini).expect("Loading conf from ini");
-        let main_conf = InternalBitcoindNetworkConfig {
-            rpc_port: 43345,
-            p2p_port: 42355,
-            prune: 15246,
-            rpc_auth: None,
-        };
-        let regtest_conf = InternalBitcoindNetworkConfig {
-            rpc_port: 34067,
-            p2p_port: 45175,
-            prune: 2043,
-            rpc_auth: Some(RpcAuth {
-                user: "my_user".to_string(),
-                salt: "my_salt".to_string(),
-                password_hmac: "my_pw_hmac".to_string(),
-            }),
-        };
-        assert_eq!(conf.networks.len(), 2);
-        assert_eq!(
-            conf.networks.get(&Network::Bitcoin).expect("Missing main"),
-            &main_conf
-        );
-        assert_eq!(
-            conf.networks
-                .get(&Network::Regtest)
-                .expect("Missing regtest"),
-            &regtest_conf
-        );
-
-        let mut conf = InternalBitcoindConfig::new();
-        conf.networks.insert(Network::Bitcoin, main_conf);
-        conf.networks.insert(Network::Regtest, regtest_conf);
-        conf_ini = conf.to_ini();
-        assert_eq!(conf_ini.len(), 3); // 2 network sections plus the empty general section
-        assert!(conf_ini.general_section().is_empty());
-        for (sec, prop) in &conf_ini {
-            if let Some(sec) = sec {
-                let rpc_port = prop.get("rpcport").expect("rpcport");
-                let p2p_port = prop.get("port").expect("port");
-                let prune = prop.get("prune").expect("prune");
-                let rpc_auth = prop.get("rpcauth");
-                if sec == "main" {
-                    assert_eq!(prop.len(), 3);
-                    assert_eq!(rpc_port, "43345");
-                    assert_eq!(p2p_port, "42355");
-                    assert_eq!(prune, "15246");
-                    assert!(rpc_auth.is_none());
-                } else if sec == "regtest" {
-                    assert_eq!(prop.len(), 4);
-                    assert_eq!(rpc_port, "34067");
-                    assert_eq!(p2p_port, "45175");
-                    assert_eq!(prune, "2043");
-                    assert_eq!(rpc_auth, Some("my_user:my_salt$my_pw_hmac"));
-                } else {
-                    panic!("Unexpected section");
-                }
-            } else {
-                assert!(prop.is_empty())
-            }
-        }
-    }
-}
diff --git a/gui/src/daemon/embedded.rs b/gui/src/daemon/embedded.rs
index 5091bb28..2978c181 100644
--- a/gui/src/daemon/embedded.rs
+++ b/gui/src/daemon/embedded.rs
@@ -64,9 +64,7 @@ impl Daemon for EmbeddedDaemon {
     }
 
     fn list_spend_txs(&self) -> Result<ListSpendResult, DaemonError> {
-        self.control()?
-            .list_spend(None)
-            .map_err(|e| DaemonError::Unexpected(e.to_string()))
+        Ok(self.control()?.list_spend())
     }
 
     fn list_confirmed_txs(
diff --git a/gui/src/installer/context.rs b/gui/src/installer/context.rs
index 6413626c..7acf49d3 100644
--- a/gui/src/installer/context.rs
+++ b/gui/src/installer/context.rs
@@ -7,7 +7,7 @@ use crate::{
         settings::{KeySetting, Settings, WalletSetting},
         wallet::wallet_name,
     },
-    bitcoind::{Bitcoind, InternalBitcoindConfig},
+    bitcoind::Bitcoind,
     hw::HardwareWalletConfig,
     signer::Signer,
 };
@@ -19,6 +19,8 @@ use liana::{
     miniscript::bitcoin,
 };
 
+use super::step::InternalBitcoindConfig;
+
 #[derive(Clone)]
 pub struct Context {
     pub bitcoin_config: BitcoinConfig,
diff --git a/gui/src/installer/step/bitcoind.rs b/gui/src/installer/step/bitcoind.rs
index 60b24853..25b70b50 100644
--- a/gui/src/installer/step/bitcoind.rs
+++ b/gui/src/installer/step/bitcoind.rs
@@ -1,3 +1,4 @@
+use std::collections::BTreeMap;
 #[cfg(target_os = "windows")]
 use std::io::{self, Cursor};
 use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener};
@@ -23,9 +24,7 @@ use liana_ui::{component::form, widget::*};
 use crate::{
     bitcoind::{
         self, bitcoind_network_dir, internal_bitcoind_datadir, internal_bitcoind_directory,
-        Bitcoind, ConfigField, InternalBitcoindConfig, InternalBitcoindConfigError,
-        InternalBitcoindNetworkConfig, RpcAuth, RpcAuthType, RpcAuthValues,
-        StartInternalBitcoindError, VERSION,
+        Bitcoind, ConfigField, RpcAuthType, RpcAuthValues, StartInternalBitcoindError, VERSION,
     },
     download,
     hw::HardwareWallets,
@@ -109,6 +108,143 @@ pub const PRUNE_DEFAULT: u32 = 15_000;
 /// Default ports used by bitcoind across all networks.
 pub const BITCOIND_DEFAULT_PORTS: [u16; 8] = [8332, 8333, 18332, 18333, 18443, 18444, 38332, 38333];
 
+/// Represents section for a single network in `bitcoin.conf` file.
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub struct InternalBitcoindNetworkConfig {
+    rpc_port: u16,
+    p2p_port: u16,
+    prune: u32,
+}
+
+/// Represents the `bitcoin.conf` file to be used by internal bitcoind.
+#[derive(Debug, Clone)]
+pub struct InternalBitcoindConfig {
+    networks: BTreeMap<Network, InternalBitcoindNetworkConfig>,
+}
+
+#[derive(PartialEq, Eq, Debug, Clone)]
+pub enum InternalBitcoindConfigError {
+    KeyNotFound(String),
+    CouldNotParseValue(String),
+    UnexpectedSection(String),
+    TooManyElements(String),
+    FileNotFound,
+    ReadingFile(String),
+    WritingFile(String),
+    Unexpected(String),
+}
+
+impl std::fmt::Display for InternalBitcoindConfigError {
+    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+        match self {
+            Self::KeyNotFound(e) => write!(f, "Config file does not contain expected key: {}", e),
+            Self::CouldNotParseValue(e) => write!(f, "Value could not be parsed: {}", e),
+            Self::UnexpectedSection(e) => write!(f, "Unexpected section in file: {}", e),
+            Self::TooManyElements(section) => {
+                write!(f, "Section in file contains too many elements: {}", section)
+            }
+            Self::FileNotFound => write!(f, "File not found"),
+            Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e),
+            Self::WritingFile(e) => write!(f, "Error while writing file: {}", e),
+            Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
+        }
+    }
+}
+
+impl Default for InternalBitcoindConfig {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl InternalBitcoindConfig {
+    pub fn new() -> Self {
+        Self {
+            networks: BTreeMap::new(),
+        }
+    }
+
+    pub fn from_ini(ini: &ini::Ini) -> Result<Self, InternalBitcoindConfigError> {
+        let mut networks = BTreeMap::new();
+        for (maybe_sec, prop) in ini {
+            if let Some(sec) = maybe_sec {
+                let network = Network::from_core_arg(sec)
+                    .map_err(|e| InternalBitcoindConfigError::UnexpectedSection(e.to_string()))?;
+                if prop.len() > 3 {
+                    return Err(InternalBitcoindConfigError::TooManyElements(
+                        sec.to_string(),
+                    ));
+                }
+                let rpc_port = prop
+                    .get("rpcport")
+                    .ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("rpcport".to_string()))?
+                    .parse::<u16>()
+                    .map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
+                let p2p_port = prop
+                    .get("port")
+                    .ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("port".to_string()))?
+                    .parse::<u16>()
+                    .map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
+                let prune = prop
+                    .get("prune")
+                    .ok_or_else(|| InternalBitcoindConfigError::KeyNotFound("prune".to_string()))?
+                    .parse::<u32>()
+                    .map_err(|e| InternalBitcoindConfigError::CouldNotParseValue(e.to_string()))?;
+                networks.insert(
+                    network,
+                    InternalBitcoindNetworkConfig {
+                        rpc_port,
+                        p2p_port,
+                        prune,
+                    },
+                );
+            } else if !prop.is_empty() {
+                return Err(InternalBitcoindConfigError::UnexpectedSection(
+                    "General section should be empty".to_string(),
+                ));
+            }
+        }
+        Ok(Self { networks })
+    }
+
+    pub fn from_file(path: &PathBuf) -> Result<Self, InternalBitcoindConfigError> {
+        if !path.exists() {
+            return Err(InternalBitcoindConfigError::FileNotFound);
+        }
+        let conf_ini = ini::Ini::load_from_file(path)
+            .map_err(|e| InternalBitcoindConfigError::ReadingFile(e.to_string()))?;
+
+        Self::from_ini(&conf_ini)
+    }
+
+    pub fn to_ini(&self) -> ini::Ini {
+        let mut conf_ini = ini::Ini::new();
+
+        for (network, network_conf) in &self.networks {
+            conf_ini
+                .with_section(Some(network.to_core_arg()))
+                .set("rpcport", network_conf.rpc_port.to_string())
+                .set("port", network_conf.p2p_port.to_string())
+                .set("prune", network_conf.prune.to_string());
+        }
+        conf_ini
+    }
+
+    pub fn to_file(&self, path: &PathBuf) -> Result<(), InternalBitcoindConfigError> {
+        std::fs::create_dir_all(
+            path.parent()
+                .ok_or_else(|| InternalBitcoindConfigError::Unexpected("No parent".to_string()))?,
+        )
+        .map_err(|e| InternalBitcoindConfigError::Unexpected(e.to_string()))?;
+        info!("Writing to file {}", path.to_string_lossy());
+        self.to_ini()
+            .write_to_file(path)
+            .map_err(|e| InternalBitcoindConfigError::WritingFile(e.to_string()))?;
+
+        Ok(())
+    }
+}
+
 #[derive(Debug)]
 pub enum InstallState {
     InProgress,
@@ -598,12 +734,9 @@ impl Step for InternalBitcoindStep {
                             return Command::none();
                         }
                     };
-                    let (rpc_port, p2p_port) = if let Some(network_conf) =
-                        conf.networks.get(&self.network)
-                    {
-                        (network_conf.rpc_port, network_conf.p2p_port)
-                    } else {
-                        match (get_available_port(), get_available_port()) {
+                    // Insert entry for network if not present.
+                    if conf.networks.get(&self.network).is_none() {
+                        let network_conf = match (get_available_port(), get_available_port()) {
                             (Ok(rpc_port), Ok(p2p_port)) => {
                                 // In case ports are the same, user will need to click button again for another attempt.
                                 if rpc_port == p2p_port {
@@ -613,7 +746,11 @@ impl Step for InternalBitcoindStep {
                                     );
                                     return Command::none();
                                 }
-                                (rpc_port, p2p_port)
+                                InternalBitcoindNetworkConfig {
+                                    rpc_port,
+                                    p2p_port,
+                                    prune: PRUNE_DEFAULT,
+                                }
                             }
                             (Ok(_), Err(e)) | (Err(e), Ok(_)) => {
                                 self.error = Some(format!("Could not get available port: {}.", e));
@@ -624,27 +761,9 @@ impl Step for InternalBitcoindStep {
                                     Some(format!("Could not get available ports: {}; {}.", e1, e2));
                                 return Command::none();
                             }
-                        }
-                    };
-                    let (rpc_auth, rpc_password) = match RpcAuth::new("liana") {
-                        Ok((rpc_auth, password)) => (rpc_auth, password),
-                        Err(e) => {
-                            self.error = Some(e.to_string());
-                            return Command::none();
-                        }
-                    };
-                    let bitcoind_config = BitcoindConfig {
-                        rpc_auth: BitcoindRpcAuth::UserPass(rpc_auth.user.clone(), rpc_password),
-                        addr: internal_bitcoind_address(rpc_port),
-                    };
-                    let network_conf = InternalBitcoindNetworkConfig {
-                        rpc_port,
-                        p2p_port,
-                        prune: PRUNE_DEFAULT,
-                        // Overwrite any previous entry for this network as we would no longer know the RPC password.
-                        rpc_auth: Some(rpc_auth),
-                    };
-                    conf.networks.insert(self.network, network_conf);
+                        };
+                        conf.networks.insert(self.network, network_conf);
+                    }
                     if let Err(e) = conf.to_file(&bitcoind::internal_bitcoind_config_path(
                         &self.bitcoind_datadir,
                     )) {
@@ -652,8 +771,7 @@ impl Step for InternalBitcoindStep {
                         return Command::none();
                     };
                     self.error = None;
-                    self.internal_bitcoind_config = Some(conf);
-                    self.bitcoind_config = Some(bitcoind_config);
+                    self.internal_bitcoind_config = Some(conf.clone());
                     return Command::perform(async {}, |_| {
                         Message::InternalBitcoind(message::InternalBitcoindMsg::Reload)
                     });
@@ -715,13 +833,31 @@ impl Step for InternalBitcoindStep {
                             StartInternalBitcoindError::CouldNotCanonicalizeDataDir(e.to_string()),
                         ));
                         return Command::none();
-                    };
-                    let bitcoind_config = self
-                        .bitcoind_config
+                    }
+
+                    let cookie_path = bitcoind::internal_bitcoind_cookie_path(
+                        &self.bitcoind_datadir,
+                        &self.network,
+                    );
+
+                    let rpc_port = self
+                        .internal_bitcoind_config
                         .as_ref()
-                        .expect("already added")
-                        .clone();
-                    match Bitcoind::start(&self.network, bitcoind_config, &self.liana_datadir) {
+                        .expect("Already added")
+                        .clone()
+                        .networks
+                        .get(&self.network)
+                        .expect("Already added")
+                        .rpc_port;
+
+                    match Bitcoind::start(
+                        &self.network,
+                        BitcoindConfig {
+                            rpc_auth: BitcoindRpcAuth::CookieFile(cookie_path),
+                            addr: internal_bitcoind_address(rpc_port),
+                        },
+                        &self.liana_datadir,
+                    ) {
                         Err(e) => {
                             self.started =
                                 Some(Err(StartInternalBitcoindError::CommandError(e.to_string())));
@@ -729,6 +865,7 @@ impl Step for InternalBitcoindStep {
                         }
                         Ok(bitcoind) => {
                             self.error = None;
+                            self.bitcoind_config = Some(bitcoind.config.clone());
                             self.started = Some(Ok(()));
                             self.internal_bitcoind = Some(bitcoind);
                         }
@@ -804,7 +941,75 @@ impl Step for InternalBitcoindStep {
 
 #[cfg(test)]
 mod tests {
-    use super::verify_hash;
+    use crate::installer::step::bitcoind::{
+        verify_hash, InternalBitcoindConfig, InternalBitcoindNetworkConfig,
+    };
+    use ini::Ini;
+    use liana::miniscript::bitcoin::Network;
+
+    // Test the format of the internal bitcoind configuration file.
+    #[test]
+    fn internal_bitcoind_config() {
+        // A valid config
+        let mut conf_ini = Ini::new();
+        conf_ini
+            .with_section(Some("main"))
+            .set("rpcport", "43345")
+            .set("port", "42355")
+            .set("prune", "15246");
+        conf_ini
+            .with_section(Some("regtest"))
+            .set("rpcport", "34067")
+            .set("port", "45175")
+            .set("prune", "2043");
+        let conf = InternalBitcoindConfig::from_ini(&conf_ini).expect("Loading conf from ini");
+        let main_conf = InternalBitcoindNetworkConfig {
+            rpc_port: 43345,
+            p2p_port: 42355,
+            prune: 15246,
+        };
+        let regtest_conf = InternalBitcoindNetworkConfig {
+            rpc_port: 34067,
+            p2p_port: 45175,
+            prune: 2043,
+        };
+        assert_eq!(conf.networks.len(), 2);
+        assert_eq!(
+            conf.networks.get(&Network::Bitcoin).expect("Missing main"),
+            &main_conf
+        );
+        assert_eq!(
+            conf.networks
+                .get(&Network::Regtest)
+                .expect("Missing regtest"),
+            &regtest_conf
+        );
+
+        let mut conf = InternalBitcoindConfig::new();
+        conf.networks.insert(Network::Bitcoin, main_conf);
+        conf.networks.insert(Network::Regtest, regtest_conf);
+        for (sec, prop) in &conf.to_ini() {
+            if let Some(sec) = sec {
+                assert_eq!(prop.len(), 3);
+                let rpc_port = prop.get("rpcport").expect("rpcport");
+                let p2p_port = prop.get("port").expect("port");
+                let prune = prop.get("prune").expect("prune");
+                if sec == "main" {
+                    assert_eq!(rpc_port, "43345");
+                    assert_eq!(p2p_port, "42355");
+                    assert_eq!(prune, "15246");
+                } else if sec == "regtest" {
+                    assert_eq!(rpc_port, "34067");
+                    assert_eq!(p2p_port, "45175");
+                    assert_eq!(prune, "2043");
+                } else {
+                    panic!("Unexpected section");
+                }
+            } else {
+                assert!(prop.is_empty())
+            }
+        }
+    }
 
     #[test]
     fn hash() {
diff --git a/gui/src/installer/step/mod.rs b/gui/src/installer/step/mod.rs
index 15529629..3583dbca 100644
--- a/gui/src/installer/step/mod.rs
+++ b/gui/src/installer/step/mod.rs
@@ -3,7 +3,8 @@ mod descriptor;
 mod mnemonic;
 
 pub use bitcoind::{
-    DefineBitcoind, DownloadState, InstallState, InternalBitcoindStep, SelectBitcoindTypeStep,
+    DefineBitcoind, DownloadState, InstallState, InternalBitcoindConfig, InternalBitcoindStep,
+    SelectBitcoindTypeStep,
 };
 
 pub use descriptor::{
diff --git a/src/bitcoin/d/mod.rs b/src/bitcoin/d/mod.rs
index faaaf371..a1b1317c 100644
--- a/src/bitcoin/d/mod.rs
+++ b/src/bitcoin/d/mod.rs
@@ -1473,7 +1473,6 @@ impl<'a> CachedTxGetter<'a> {
 #[derive(Debug, Clone)]
 pub struct MempoolEntry {
     pub vsize: u64,
-    pub ancestor_vsize: u64,
     pub fees: MempoolEntryFees,
 }
 
@@ -1483,28 +1482,19 @@ impl From<Json> for MempoolEntry {
             .get("vsize")
             .and_then(Json::as_u64)
             .expect("Must be present in bitcoind response");
-        let ancestor_vsize = json
-            .get("ancestorsize")
-            .and_then(Json::as_u64)
-            .expect("Must be present in bitcoind response");
         let fees = json
             .get("fees")
             .as_ref()
             .expect("Must be present in bitcoind response")
             .into();
 
-        MempoolEntry {
-            vsize,
-            ancestor_vsize,
-            fees,
-        }
+        MempoolEntry { vsize, fees }
     }
 }
 
 #[derive(Debug, Clone)]
 pub struct MempoolEntryFees {
     pub base: bitcoin::Amount,
-    pub ancestor: bitcoin::Amount,
     pub descendant: bitcoin::Amount,
 }
 
@@ -1516,20 +1506,11 @@ impl From<&&Json> for MempoolEntryFees {
             .and_then(Json::as_f64)
             .and_then(|a| bitcoin::Amount::from_btc(a).ok())
             .expect("Must be present and a valid amount");
-        let ancestor = json
-            .get("ancestor")
-            .and_then(Json::as_f64)
-            .and_then(|a| bitcoin::Amount::from_btc(a).ok())
-            .expect("Must be present and a valid amount");
         let descendant = json
             .get("descendant")
             .and_then(Json::as_f64)
             .and_then(|a| bitcoin::Amount::from_btc(a).ok())
             .expect("Must be present and a valid amount");
-        MempoolEntryFees {
-            base,
-            ancestor,
-            descendant,
-        }
+        MempoolEntryFees { base, descendant }
     }
 }
diff --git a/src/bitcoin/mod.rs b/src/bitcoin/mod.rs
index a278d33a..60388c1e 100644
--- a/src/bitcoin/mod.rs
+++ b/src/bitcoin/mod.rs
@@ -40,8 +40,6 @@ 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.
@@ -118,23 +116,9 @@ pub trait BitcoinInterface: Send {
 
     /// Get the details of unconfirmed transactions spending these outpoints, if any.
     fn mempool_spenders(&self, outpoints: &[bitcoin::OutPoint]) -> Vec<MempoolEntry>;
-
-    /// Get mempool data for the given transaction.
-    ///
-    /// Returns `None` if the transaction is not in the mempool.
-    fn mempool_entry(&self, txid: &bitcoin::Txid) -> Option<MempoolEntry>;
 }
 
 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
@@ -382,18 +366,10 @@ impl BitcoinInterface for d::BitcoinD {
             .filter_map(|txid| self.mempool_entry(&txid))
             .collect()
     }
-
-    fn mempool_entry(&self, txid: &bitcoin::Txid) -> Option<MempoolEntry> {
-        self.mempool_entry(txid)
-    }
 }
 
 // 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()
     }
@@ -480,10 +456,6 @@ impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>>
     fn mempool_spenders(&self, outpoints: &[bitcoin::OutPoint]) -> Vec<MempoolEntry> {
         self.lock().unwrap().mempool_spenders(outpoints)
     }
-
-    fn mempool_entry(&self, txid: &bitcoin::Txid) -> Option<MempoolEntry> {
-        self.lock().unwrap().mempool_entry(txid)
-    }
 }
 
 // FIXME: We could avoid this type (and all the conversions entailing allocations) if bitcoind
diff --git a/src/bitcoin/poller/looper.rs b/src/bitcoin/poller/looper.rs
index 0b984d86..cdff3920 100644
--- a/src/bitcoin/poller/looper.rs
+++ b/src/bitcoin/poller/looper.rs
@@ -245,8 +245,8 @@ fn updates(
     db_conn.new_unspent_coins(&updated_coins.received);
     db_conn.remove_coins(&updated_coins.expired);
     db_conn.confirm_coins(&updated_coins.confirmed);
-    db_conn.spend_coins(&updated_coins.spending);
     db_conn.unspend_coins(&updated_coins.expired_spending);
+    db_conn.spend_coins(&updated_coins.spending);
     db_conn.confirm_spend(&updated_coins.spent);
     if latest_tip != current_tip {
         db_conn.update_tip(&latest_tip);
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
index 9a4bf835..bf473bf6 100644
--- a/src/commands/mod.rs
+++ b/src/commands/mod.rs
@@ -9,7 +9,7 @@ use crate::{
     database::{Coin, DatabaseConnection, DatabaseInterface},
     descriptors,
     spend::{
-        create_spend, AddrInfo, AncestorInfo, CandidateCoin, CreateSpendRes, SpendCreationError,
+        create_spend, AddrInfo, CandidateCoin, CreateSpendRes, SpendCreationError,
         SpendOutputAddress, SpendTxFees, TxGetter,
     },
     DaemonControl, VERSION,
@@ -33,6 +33,9 @@ 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,
@@ -59,7 +62,6 @@ pub enum CommandError {
     /// Overflowing or unhardened derivation index.
     InvalidDerivationIndex,
     RbfError(RbfErrorInfo),
-    EmptyFilterList,
 }
 
 impl fmt::Display for CommandError {
@@ -112,7 +114,6 @@ impl fmt::Display for CommandError {
                 write!(f, "Unhardened or overflowing BIP32 derivation index.")
             }
             Self::RbfError(e) => write!(f, "RBF error: '{}'.", e),
-            Self::EmptyFilterList => write!(f, "Filter list is empty, should supply None instead."),
         }
     }
 }
@@ -178,7 +179,6 @@ fn coin_to_candidate(
     coin: &Coin,
     must_select: bool,
     sequence: Option<bitcoin::Sequence>,
-    ancestor_info: Option<AncestorInfo>,
 ) -> CandidateCoin {
     CandidateCoin {
         outpoint: coin.outpoint,
@@ -187,7 +187,6 @@ fn coin_to_candidate(
         is_change: coin.is_change,
         must_select,
         sequence,
-        ancestor_info,
     }
 }
 
@@ -262,12 +261,12 @@ impl DaemonControl {
         addr_info: &Option<AddrInfo>,
     ) {
         if let Some(AddrInfo { index, is_change }) = addr_info {
-            if *is_change && db_conn.change_index() <= *index {
+            if *is_change && db_conn.change_index() < *index {
                 let next_index = index
                     .increment()
                     .expect("Must not get into hardened territory");
                 db_conn.set_change_index(next_index, &self.secp);
-            } else if !is_change && db_conn.receive_index() <= *index {
+            } else if !is_change && db_conn.receive_index() < *index {
                 let next_index = index
                     .increment()
                     .expect("Must not get into hardened territory");
@@ -461,33 +460,13 @@ impl DaemonControl {
         // the spend from a set of optional candidates.
         // Otherwise, only the specified coins will be used, all as mandatory candidates.
         let candidate_coins: Vec<CandidateCoin> = if coins_outpoints.is_empty() {
-            // From our unconfirmed coins, we only include those that are change outputs
-            // since unconfirmed external deposits are more at risk of being dropped
-            // unexpectedly from the mempool as they are beyond the user's control.
+            // We only select confirmed coins for now. Including unconfirmed ones as well would
+            // introduce a whole bunch of additional complexity.
             db_conn
-                .coins(&[CoinStatus::Unconfirmed, CoinStatus::Confirmed], &[])
-                .into_iter()
-                .filter_map(|(op, c)| {
-                    if c.block_info.is_some() {
-                        Some((c, None)) // confirmed coins have no ancestor info
-                    } else if c.is_change {
-                        // In case the mempool_entry is None, the coin will be included without
-                        // any ancestor info.
-                        Some((
-                            c,
-                            self.bitcoin.mempool_entry(&op.txid).map(AncestorInfo::from),
-                        ))
-                    } else {
-                        None
-                    }
-                })
-                .map(|(c, ancestor_info)| {
-                    coin_to_candidate(
-                        &c,
-                        /*must_select=*/ false,
-                        /*sequence=*/ None,
-                        ancestor_info,
-                    )
+                .coins(&[CoinStatus::Confirmed], &[])
+                .into_values()
+                .map(|c| {
+                    coin_to_candidate(&c, /*must_select=*/ false, /*sequence=*/ None)
                 })
                 .collect()
         } else {
@@ -503,23 +482,8 @@ impl DaemonControl {
                 }
             }
             coins
-                .into_iter()
-                .map(|(op, c)| {
-                    let ancestor_info = if c.block_info.is_none() {
-                        // We include any non-change coins here as they have been selected by the caller.
-                        // If the unconfirmed coin's transaction is no longer in the mempool, keep the
-                        // coin as a candidate but without any ancestor info (same as confirmed candidate).
-                        self.bitcoin.mempool_entry(&op.txid).map(AncestorInfo::from)
-                    } else {
-                        None
-                    };
-                    coin_to_candidate(
-                        &c,
-                        /*must_select=*/ true,
-                        /*sequence=*/ None,
-                        ancestor_info,
-                    )
-                })
+                .into_values()
+                .map(|c| coin_to_candidate(&c, /*must_select=*/ true, /*sequence=*/ None))
                 .collect()
         };
 
@@ -625,32 +589,14 @@ impl DaemonControl {
         }
     }
 
-    pub fn list_spend(
-        &self,
-        txids: Option<Vec<bitcoin::Txid>>,
-    ) -> Result<ListSpendResult, CommandError> {
-        if let Some(ids) = &txids {
-            if ids.is_empty() {
-                return Err(CommandError::EmptyFilterList);
-            }
-        }
-
+    pub fn list_spend(&self) -> ListSpendResult {
         let mut db_conn = self.db.connection();
-        let spend_psbts = db_conn.list_spend();
-
-        let txids_set: Option<HashSet<_>> = txids.as_ref().map(|list| list.iter().collect());
-        let spend_txs = spend_psbts
+        let spend_txs = db_conn
+            .list_spend()
             .into_iter()
-            .filter_map(|(psbt, updated_at)| {
-                if let Some(set) = &txids_set {
-                    if !set.contains(&psbt.unsigned_tx.txid()) {
-                        return None;
-                    }
-                }
-                Some(ListSpendEntry { psbt, updated_at })
-            })
+            .map(|(psbt, updated_at)| ListSpendEntry { psbt, updated_at })
             .collect();
-        Ok(ListSpendResult { spend_txs })
+        ListSpendResult { spend_txs }
     }
 
     pub fn delete_spend(&self, txid: &bitcoin::Txid) {
@@ -849,14 +795,7 @@ impl DaemonControl {
         let mut candidate_coins: Vec<CandidateCoin> = prev_coins
             .values()
             .map(|c| {
-                // In case any previous coins are unconfirmed, we don't include their ancestor info
-                // in the candidate as the replacement fee and feerate will be higher and any
-                // additional fee to pay for ancestors should already have been taken into account
-                // when including these coins in the previous transaction.
-                coin_to_candidate(
-                    c, /*must_select=*/ !is_cancel, /*sequence=*/ None,
-                    /*ancestor_info=*/ None,
-                )
+                coin_to_candidate(c, /*must_select=*/ !is_cancel, /*sequence=*/ None)
             })
             .collect();
         let confirmed_cands: Vec<CandidateCoin> = db_conn
@@ -868,7 +807,6 @@ impl DaemonControl {
                 if !prev_coins.contains_key(&c.outpoint) {
                     Some(coin_to_candidate(
                         &c, /*must_select=*/ false, /*sequence=*/ None,
-                        /*ancestor_info=*/ None,
                     ))
                 } else {
                     None
@@ -950,14 +888,13 @@ 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 < genesis_timestamp || future_timestamp {
+        if timestamp < MAINNET_GENESIS_TIME || future_timestamp {
             return Err(CommandError::InsaneRescanTimestamp(timestamp));
         }
         if db_conn.rescan_timestamp().is_some() || self.bitcoin.rescan_progress().is_some() {
@@ -1059,7 +996,6 @@ impl DaemonControl {
                         &c,
                         /*must_select=*/ true,
                         /*sequence=*/ Some(bitcoin::Sequence::from_height(timelock)),
-                        /*ancestor_info=*/ None,
                     ))
                 } else {
                     None
@@ -1448,8 +1384,8 @@ mod tests {
             spend_txid: None,
             spend_block: None,
         }]);
-        // If we try to use coin selection, the unconfirmed non-change coin will not be used
-        // as a candidate and so we get a coin selection error due to insufficient funds.
+        // If we try to use coin selection, the unconfirmed coin will not be used as a candidate
+        // and so we get a coin selection error due to insufficient funds.
         assert!(matches!(
             control.create_spend(&destinations, &[], 1, None),
             Ok(CreateSpendResult::InsufficientFunds { .. }),
@@ -1680,7 +1616,7 @@ mod tests {
             )))
         );
 
-        // Add an unconfirmed change coin to be used for coin selection.
+        // Add a confirmed unspent coin to be used for coin selection.
         let confirmed_op_1 = bitcoin::OutPoint {
             txid: dummy_op.txid,
             vout: dummy_op.vout + 100,
@@ -1688,10 +1624,13 @@ mod tests {
         db_conn.new_unspent_coins(&[Coin {
             outpoint: confirmed_op_1,
             is_immature: false,
-            block_info: None,
+            block_info: Some(BlockInfo {
+                height: 174500,
+                time: 174500,
+            }),
             amount: bitcoin::Amount::from_sat(80_000),
             derivation_index: bip32::ChildNumber::from(42),
-            is_change: true,
+            is_change: false,
             spend_txid: None,
             spend_block: None,
         }]);
@@ -1760,46 +1699,9 @@ mod tests {
             panic!("expect successful spend creation")
         };
         let tx_manual = psbt.unsigned_tx;
-        // Check that manual and auto selection give same outputs (except change address).
-        assert_ne!(tx_auto.output, tx_manual.output);
-        assert_eq!(tx_auto.output.len(), tx_manual.output.len());
-        assert_eq!(tx_auto.output[0], tx_manual.output[0]);
-        assert_eq!(tx_auto.output[1].value, tx_manual.output[1].value);
-        assert_ne!(
-            tx_auto.output[1].script_pubkey,
-            tx_manual.output[1].script_pubkey
-        );
-        // Check inputs are also the same. Need to sort as order is not guaranteed by `create_spend`.
-        let mut auto_input = tx_auto.clone().input;
-        let mut manual_input = tx_manual.input;
-        auto_input.sort();
-        manual_input.sort();
-        assert_eq!(auto_input, manual_input);
-
-        // Now do the same again, but this time specifying the change address to be the same
-        // as for the auto spend.
-        let change_address = bitcoin::Address::from_script(
-            tx_auto.output[1].script_pubkey.as_script(),
-            bitcoin::Network::Bitcoin,
-        )
-        .unwrap();
-        let psbt = if let CreateSpendResult::Success { psbt, .. } = control
-            .create_spend(
-                &destinations,
-                &[confirmed_op_1, confirmed_op_2],
-                1,
-                Some(change_address.as_unchecked().clone()),
-            )
-            .unwrap()
-        {
-            psbt
-        } else {
-            panic!("expect successful spend creation")
-        };
-        let tx_manual = psbt.unsigned_tx;
-        // Now the outputs of each transaction are the same.
+        // Check that manual and auto selection give same outputs (including change).
         assert_eq!(tx_auto.output, tx_manual.output);
-        // Check again that inputs are still the same.
+        // Check inputs are also the same. Need to sort as order is not guaranteed by `create_spend`.
         let mut auto_input = tx_auto.input;
         let mut manual_input = tx_manual.input;
         auto_input.sort();
diff --git a/src/database/mod.rs b/src/database/mod.rs
index cf06eaf4..050581d5 100644
--- a/src/database/mod.rs
+++ b/src/database/mod.rs
@@ -398,17 +398,6 @@ impl CoinStatus {
             _ => None,
         }
     }
-
-    /// Converts a `CoinStatus` to its equivalent argument name
-    /// as used in the `listcoins` RPC command.
-    pub fn to_arg(&self) -> &'static str {
-        match self {
-            CoinStatus::Unconfirmed => "unconfirmed",
-            CoinStatus::Confirmed => "confirmed",
-            CoinStatus::Spending => "spending",
-            CoinStatus::Spent => "spent",
-        }
-    }
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -463,28 +452,3 @@ impl LabelItem {
         }
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn coin_status_as_arg() {
-        assert_eq!(
-            CoinStatus::from_arg(CoinStatus::Unconfirmed.to_arg()),
-            Some(CoinStatus::Unconfirmed)
-        );
-        assert_eq!(
-            CoinStatus::from_arg(CoinStatus::Confirmed.to_arg()),
-            Some(CoinStatus::Confirmed)
-        );
-        assert_eq!(
-            CoinStatus::from_arg(CoinStatus::Spending.to_arg()),
-            Some(CoinStatus::Spending)
-        );
-        assert_eq!(
-            CoinStatus::from_arg(CoinStatus::Spent.to_arg()),
-            Some(CoinStatus::Spent)
-        );
-    }
-}
diff --git a/src/jsonrpc/api.rs b/src/jsonrpc/api.rs
index 8bc7fcab..f99ee9ef 100644
--- a/src/jsonrpc/api.rs
+++ b/src/jsonrpc/api.rs
@@ -10,7 +10,7 @@ use std::{
     str::FromStr,
 };
 
-use miniscript::bitcoin::{self, psbt::Psbt, Txid};
+use miniscript::bitcoin::{self, psbt::Psbt};
 
 fn create_spend(control: &DaemonControl, params: Params) -> Result<serde_json::Value, Error> {
     let destinations = params
@@ -226,32 +226,6 @@ fn list_confirmed(control: &DaemonControl, params: Params) -> Result<serde_json:
     ))
 }
 
-fn list_spendtxs(
-    control: &DaemonControl,
-    params: Option<Params>,
-) -> Result<serde_json::Value, Error> {
-    let txids: Option<Vec<bitcoin::Txid>> = if let Some(p) = params {
-        let tx_ids = p.get(0, "txids");
-        if let Some(ids) = tx_ids {
-            let ids: Vec<Txid> = ids
-                .as_array()
-                .and_then(|arr| {
-                    arr.iter()
-                        .map(|entry| entry.as_str().and_then(|e| bitcoin::Txid::from_str(e).ok()))
-                        .collect()
-                })
-                .ok_or_else(|| Error::invalid_params("Invalid 'txids' parameter."))?;
-            Some(ids)
-        } else {
-            None
-        }
-    } else {
-        None
-    };
-
-    Ok(serde_json::json!(&control.list_spend(txids)?))
-}
-
 fn list_transactions(control: &DaemonControl, params: Params) -> Result<serde_json::Value, Error> {
     let txids: Vec<bitcoin::Txid> = params
         .get(0, "txids")
@@ -417,7 +391,7 @@ pub fn handle_request(control: &DaemonControl, req: Request) -> Result<Response,
             })?;
             list_confirmed(control, params)?
         }
-        "listspendtxs" => list_spendtxs(control, req.params)?,
+        "listspendtxs" => serde_json::json!(&control.list_spend()),
         "listtransactions" => {
             let params = req.params.ok_or_else(|| {
                 Error::invalid_params(
diff --git a/src/jsonrpc/mod.rs b/src/jsonrpc/mod.rs
index 131a96a6..e21ac0e7 100644
--- a/src/jsonrpc/mod.rs
+++ b/src/jsonrpc/mod.rs
@@ -165,7 +165,6 @@ impl From<commands::CommandError> for Error {
             | commands::CommandError::AlreadyRescanning
             | commands::CommandError::InvalidDerivationIndex
             | commands::CommandError::RbfError(..)
-            | commands::CommandError::EmptyFilterList
             | commands::CommandError::RecoveryNotAvailable => {
                 Error::new(ErrorCode::InvalidParams, e.to_string())
             }
diff --git a/src/lib.rs b/src/lib.rs
index c3a3b52e..a58b6b30 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -7,7 +7,7 @@ mod database;
 pub mod descriptors;
 #[cfg(feature = "daemon")]
 mod jsonrpc;
-pub mod random;
+mod random;
 pub mod signer;
 pub mod spend;
 #[cfg(test)]
diff --git a/src/spend.rs b/src/spend.rs
index d545375e..4b9435d7 100644
--- a/src/spend.rs
+++ b/src/spend.rs
@@ -1,10 +1,6 @@
-use crate::{bitcoin::MempoolEntry, descriptors};
+use crate::descriptors;
 
-use std::{
-    collections::{BTreeMap, HashMap},
-    convert::TryInto,
-    fmt,
-};
+use std::{collections::BTreeMap, convert::TryInto, fmt};
 
 pub use bdk_coin_select::InsufficientFunds;
 use bdk_coin_select::{
@@ -15,7 +11,6 @@ use miniscript::bitcoin::{
     self,
     absolute::{Height, LockTime},
     bip32,
-    constants::WITNESS_SCALE_FACTOR,
     psbt::{Input as PsbtIn, Output as PsbtOut, Psbt},
     secp256k1,
 };
@@ -159,29 +154,6 @@ fn sanity_check_psbt(
     Ok(())
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub struct AncestorInfo {
-    pub vsize: u32,
-    pub fee: u32,
-}
-
-impl From<MempoolEntry> for AncestorInfo {
-    fn from(entry: MempoolEntry) -> Self {
-        Self {
-            vsize: entry
-                .ancestor_vsize
-                .try_into()
-                .expect("vsize must fit in a u32"),
-            fee: entry
-                .fees
-                .ancestor
-                .to_sat()
-                .try_into()
-                .expect("fee in sats should fit in a u32"),
-        }
-    }
-}
-
 /// A candidate for coin selection when creating a transaction.
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
 pub struct CandidateCoin {
@@ -197,8 +169,6 @@ pub struct CandidateCoin {
     pub must_select: bool,
     /// The nSequence field to set for an input spending this coin.
     pub sequence: Option<bitcoin::Sequence>,
-    /// Information about in-mempool ancestors of the coin.
-    pub ancestor_info: Option<AncestorInfo>,
 }
 
 /// A coin selection result.
@@ -214,8 +184,6 @@ pub struct CoinSelectionRes {
     /// Maximum change amount possible with the selection irrespective of any change
     /// policy.
     pub max_change_amount: bitcoin::Amount,
-    /// Fee added to pay for ancestors at the target feerate.
-    pub fee_for_ancestors: bitcoin::Amount,
 }
 
 /// Metric based on [`LowestFee`] that aims to minimize transaction fees
@@ -323,48 +291,12 @@ fn select_coins_for_spend(
         base_weight = base_weight.saturating_sub(2);
     }
     let max_input_weight = TXIN_BASE_WEIGHT + max_sat_weight;
-    // Get feerate as u32 for calculation relating to ancestor below.
-    // We expect `feerate_vb` to be a positive integer, but take ceil()
-    // just in case to be sure we pay enough for ancestors.
-    let feerate_vb_u32 = feerate_vb.ceil() as u32;
-    let witness_factor: u32 = WITNESS_SCALE_FACTOR
-        .try_into()
-        .expect("scale factor must fit in u32");
-    // This will be used to store any extra weight added to candidates.
-    let mut added_weights = HashMap::<bitcoin::OutPoint, u32>::with_capacity(candidate_coins.len());
     let candidates: Vec<Candidate> = candidate_coins
         .iter()
         .map(|cand| Candidate {
             input_count: 1,
             value: cand.amount.to_sat(),
-            weight: {
-                let extra = cand
-                    .ancestor_info
-                    .map(|info| {
-                        // The implied ancestor vsize if the fee had been paid at our target feerate.
-                        let ancestor_vsize_at_feerate: u32 = info
-                            .fee
-                            .checked_div(feerate_vb_u32)
-                            .expect("feerate is greater than zero");
-                        // If the actual ancestor vsize is bigger than the implied vsize, we will need to
-                        // pay the difference in order for the combined feerate to be at the target value.
-                        // We multiply the vsize by 4 to get the ancestor weight, which is an upper bound
-                        // on its true weight (vsize*4 - 3 <= weight <= vsize*4), to ensure we pay enough.
-                        // Note that if candidates share ancestors, we may add this difference more than
-                        // once in the resulting transaction.
-                        info.vsize
-                            .saturating_sub(ancestor_vsize_at_feerate)
-                            .checked_mul(witness_factor)
-                            .expect("weight difference must fit in u32")
-                    })
-                    .unwrap_or(0);
-                // Store the extra weight for this candidate for use later on.
-                // At the same time, make sure there are no duplicate outpoints.
-                assert!(added_weights.insert(cand.outpoint, extra).is_none());
-                max_input_weight
-                    .checked_add(extra)
-                    .expect("effective weight must fit in u32")
-            },
+            weight: max_input_weight,
             is_segwit: true, // We only support receiving on Segwit scripts.
         })
         .collect();
@@ -404,10 +336,9 @@ fn select_coins_for_spend(
 
     // Finally, run the coin selection algorithm. We use an opportunistic BnB and if it couldn't
     // find any solution we fall back to selecting coins by descending value.
-    let feerate = FeeRate::from_sat_per_vb(feerate_vb);
     let target = Target {
         value: out_value_nochange,
-        feerate,
+        feerate: FeeRate::from_sat_per_vb(feerate_vb),
         min_fee,
     };
     let lowest_fee = LowestFee {
@@ -471,29 +402,14 @@ fn select_coins_for_spend(
             .try_into()
             .expect("value is non-negative"),
     );
-    let mut total_added_weight: u32 = 0;
-    let selected = selector
-        .selected_indices()
-        .iter()
-        .map(|i| candidate_coins[*i])
-        .inspect(|cand| {
-            total_added_weight = total_added_weight
-                .checked_add(
-                    *added_weights
-                        .get(&cand.outpoint)
-                        .expect("contains added weight for all candidates"),
-                )
-                .expect("should fit in u32")
-        })
-        .collect();
-    // Calculate added fee based on the feerate in sats/wu, which is the feerate used for coin selection.
-    let fee_for_ancestors =
-        bitcoin::Amount::from_sat(((total_added_weight as f32) * feerate.spwu()).ceil() as u64);
     Ok(CoinSelectionRes {
-        selected,
+        selected: selector
+            .selected_indices()
+            .iter()
+            .map(|i| candidate_coins[*i])
+            .collect(),
         change_amount,
         max_change_amount,
-        fee_for_ancestors,
     })
 }
 
@@ -543,7 +459,6 @@ pub enum SpendTxFees {
 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
 pub enum CreateSpendWarning {
     ChangeAddedToFee(u64),
-    AddtionalFeeForAncestors(u64),
 }
 
 impl fmt::Display for CreateSpendWarning {
@@ -555,12 +470,6 @@ impl fmt::Display for CreateSpendWarning {
                 amt,
                 if *amt > 1 {"s"} else {""},
             ),
-            CreateSpendWarning::AddtionalFeeForAncestors(amt) => write!(
-                f,
-                "An additional fee of {} sat{} has been added to pay for ancestors at the target feerate.",
-                amt,
-                if *amt > 1 {"s"} else {""},
-            ),
         }
     }
 }
@@ -680,7 +589,6 @@ pub fn create_spend(
         selected,
         change_amount,
         max_change_amount,
-        fee_for_ancestors,
     } = {
         // At this point the transaction still has no input and no change output, as expected
         // by the coins selection helper function.
@@ -742,12 +650,6 @@ pub fn create_spend(
         ));
     }
 
-    if fee_for_ancestors.to_sat() > 0 {
-        warnings.push(CreateSpendWarning::AddtionalFeeForAncestors(
-            fee_for_ancestors.to_sat(),
-        ));
-    }
-
     // Iterate through selected coins and add necessary information to the PSBT inputs.
     let mut psbt_ins = Vec::with_capacity(selected.len());
     for cand in &selected {


Test patch:
diff --git a/src/testutils.rs b/src/testutils.rs
index 2bee5012..9034aaec 100644
--- a/src/testutils.rs
+++ b/src/testutils.rs
@@ -34,10 +34,6 @@ 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",
@@ -127,10 +123,6 @@ impl BitcoinInterface for DummyBitcoind {
     fn mempool_spenders(&self, _: &[bitcoin::OutPoint]) -> Vec<MempoolEntry> {
         Vec::new()
     }
-
-    fn mempool_entry(&self, _: &bitcoin::Txid) -> Option<MempoolEntry> {
-        None
-    }
 }
 
 struct DummyDbState {
@@ -212,7 +204,7 @@ impl DatabaseConnection for DummyDatabase {
     }
 
     fn change_index(&mut self) -> bip32::ChildNumber {
-        self.db.read().unwrap().change_index
+        self.db.read().unwrap().deposit_index
     }
 
     fn set_change_index(
diff --git a/tests/test_chain.py b/tests/test_chain.py
index e93a088a..1ed1c133 100644
--- a/tests/test_chain.py
+++ b/tests/test_chain.py
@@ -3,6 +3,7 @@ import copy
 from fixtures import *
 from test_framework.utils import (
     wait_for,
+    wait_for_while_condition_holds,
     get_txid,
     spend_coins,
     RpcError,
@@ -407,7 +408,13 @@ def test_conflicting_unconfirmed_spend_txs(lianad, bitcoind):
             return False
         return coin["spend_info"]["txid"] == txid.hex()
 
-    wait_for(lambda: is_spent_by(lianad, spent_coin["outpoint"], txid_b))
+    wait_for_while_condition_holds(
+        lambda: is_spent_by(lianad, spent_coin["outpoint"], txid_b),
+        lambda: lianad.rpc.listcoins([], [spent_coin["outpoint"]])["coins"][0][
+            "spend_info"
+        ]
+        is not None,  # The spend txid changes directly from txid_a to txid_b
+    )
 
 
 def test_spend_replacement(lianad, bitcoind):
@@ -454,11 +461,15 @@ def test_spend_replacement(lianad, bitcoind):
     # newly marked as spending, the second one's spend_txid should be updated and
     # the first one's spend txid should be dropped.
     second_txid = sign_and_broadcast_psbt(lianad, second_psbt)
-    wait_for(
+    wait_for_while_condition_holds(
         lambda: all(
             c["spend_info"] is not None and c["spend_info"]["txid"] == second_txid
             for c in lianad.rpc.listcoins([], second_outpoints)["coins"]
-        )
+        ),
+        lambda: lianad.rpc.listcoins([], [coins[1]["outpoint"]])["coins"][0][
+            "spend_info"
+        ]
+        is not None,  # The spend txid of coin from first spend is updated directly
     )
     wait_for(
         lambda: lianad.rpc.listcoins([], [first_outpoints[0]])["coins"][0]["spend_info"]
@@ -467,11 +478,15 @@ def test_spend_replacement(lianad, bitcoind):
 
     # Now RBF the second transaction with a send-to-self, just because.
     third_txid = sign_and_broadcast_psbt(lianad, third_psbt)
-    wait_for(
+    wait_for_while_condition_holds(
         lambda: all(
             c["spend_info"] is not None and c["spend_info"]["txid"] == third_txid
             for c in lianad.rpc.listcoins([], second_outpoints)["coins"]
-        )
+        ),
+        lambda: all(
+            c["spend_info"] is not None
+            for c in lianad.rpc.listcoins([], second_outpoints)["coins"]
+        ),  # The spend txid of all coins are updated directly
     )
     assert (
         lianad.rpc.listcoins([], [first_outpoints[0]])["coins"][0]["spend_info"] is None
diff --git a/tests/test_framework/utils.py b/tests/test_framework/utils.py
index 8829fd5e..4aabf759 100644
--- a/tests/test_framework/utils.py
+++ b/tests/test_framework/utils.py
@@ -35,17 +35,33 @@ def wait_for(success, timeout=TIMEOUT, debug_fn=None):
     debug_fn is logged at each call to success, it can be useful for debugging
     when tests fail.
     """
+    wait_for_while_condition_holds(success, lambda: True, timeout, debug_fn)
+
+
+def wait_for_while_condition_holds(success, condition, timeout=TIMEOUT, debug_fn=None):
+    """
+    Run success() either until it returns True, or until the timeout is reached,
+    as long as condition() holds.
+    debug_fn is logged at each call to success, it can be useful for debugging
+    when tests fail.
+    """
     start_time = time.time()
     interval = 0.25
-    while not success() and time.time() < start_time + timeout:
+    while True:
+        if time.time() >= start_time + timeout:
+            raise ValueError("Error waiting for {}", success)
+        if not condition():
+            raise ValueError(
+                "Condition {} not met while waiting for {}", condition, success
+            )
+        if success():
+            return
         if debug_fn is not None:
             logging.info(debug_fn())
         time.sleep(interval)
         interval *= 2
         if interval > 5:
             interval = 5
-    if time.time() > start_time + timeout:
-        raise ValueError("Error waiting for {}", success)
 
 
 def get_txid(hex_tx):
diff --git a/tests/test_rpc.py b/tests/test_rpc.py
index 87e74af9..904d17b8 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)
@@ -458,35 +458,6 @@ def test_list_spend(lianad, bitcoind):
     lianad.rpc.updatespend(res["psbt"])
     lianad.rpc.updatespend(res_b["psbt"])
 
-    # Check 'txids' parameter
-    list_res = lianad.rpc.listspendtxs()["spend_txs"]
-
-    txid = PSBT.from_base64(list_res[0]["psbt"]).tx.txid().hex()
-
-    filtered_res = lianad.rpc.listspendtxs(txids=[txid])
-    assert filtered_res["spend_txs"][0]["psbt"] == list_res[0]["psbt"]
-    assert len(filtered_res) == 1
-
-    with pytest.raises(
-        RpcError, match="Filter list is empty, should supply None instead."
-    ):
-        lianad.rpc.listspendtxs(txids=[])
-
-    with pytest.raises(RpcError, match="Invalid params: Invalid 'txids' parameter."):
-        lianad.rpc.listspendtxs(txids=[txid, 123])
-
-    with pytest.raises(RpcError, match="Invalid params: Invalid 'txids' parameter."):
-        lianad.rpc.listspendtxs(txids=[0])
-
-    with pytest.raises(RpcError, match="Invalid params: Invalid 'txids' parameter."):
-        lianad.rpc.listspendtxs(txids=[123])
-
-    with pytest.raises(RpcError, match="Invalid params: Invalid 'txids' parameter."):
-        lianad.rpc.listspendtxs(txids=["abc"])
-
-    with pytest.raises(RpcError, match="Invalid params: Invalid 'txids' parameter."):
-        lianad.rpc.listspendtxs(txids=["123"])
-
     # Listing all Spend transactions will list them both. It'll tell us which one has
     # change and which one doesn't.
     list_res = lianad.rpc.listspendtxs()["spend_txs"]
@@ -628,15 +599,10 @@ 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
-    block_hash = bitcoind.rpc.getblockhash(0)
-    genesis_timestamp = bitcoind.rpc.getblock(block_hash)["time"]
-    prebitcoin_timestamp = genesis_timestamp - 1
+    prebitcoin_timestamp = 1231006505 - 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):
@@ -1266,7 +1232,9 @@ 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"])
diff --git a/tests/test_spend.py b/tests/test_spend.py
index 0cf4ab12..4ae75229 100644
--- a/tests/test_spend.py
+++ b/tests/test_spend.py
@@ -1,6 +1,6 @@
 from fixtures import *
 from test_framework.serializations import PSBT, uint256_from_str
-from test_framework.utils import sign_and_broadcast_psbt, wait_for, COIN, RpcError
+from test_framework.utils import wait_for, COIN, RpcError
 
 
 def test_spend_change(lianad, bitcoind):
@@ -59,6 +59,14 @@ def test_spend_change(lianad, bitcoind):
     bitcoind.generate_block(1, wait_for_mempool=spend_txid)
 
 
+def sign_and_broadcast_psbt(lianad, psbt):
+    txid = psbt.tx.txid().hex()
+    psbt = lianad.signer.sign_psbt(psbt)
+    lianad.rpc.updatespend(psbt.to_base64())
+    lianad.rpc.broadcastspend(txid)
+    return txid
+
+
 def test_coin_marked_spent(lianad, bitcoind):
     """Test a spent coin is marked as such under various conditions."""
     # Receive a coin in a single transaction
@@ -171,16 +179,7 @@ def test_coin_marked_spent(lianad, bitcoind):
     psbt = PSBT.from_base64(res["psbt"])
     sign_and_broadcast(psbt)
     assert len(psbt.o) == 4
-    assert bitcoind.rpc.getmempoolentry(deposit_d)["ancestorsize"] == 165
-    assert bitcoind.rpc.getmempoolentry(deposit_d)["fees"]["ancestor"] * COIN == 165
-    # ancestor vsize at feerate 2 sat/vb = ancestor_fee / 2 = 165 / 2 = 82
-    # extra_weight <= (extra vsize * witness factor) = (165 - 82) * 4 = 332
-    # additional fee at 2 sat/vb (0.5 sat/wu) = 332 * 0.5 = 166
-    assert len(res["warnings"]) == 1
-    assert (
-        res["warnings"][0]
-        == "An additional fee of 166 sats has been added to pay for ancestors at the target feerate."
-    )
+    assert len(res["warnings"]) == 0
 
     # All the spent coins must have been detected as such
     all_deposits = (deposit_a, deposit_b, deposit_c, deposit_d)
@@ -246,200 +245,88 @@ def test_send_to_self(lianad, bitcoind):
     )
     wait_for(lambda: len(list(unspent_coins())) == 1)
 
-    # We've used 3 receive addresses and so the DB receive index must be 3.
-    assert len(lianad.rpc.listaddresses()["addresses"]) == 3
-    # Create a new spend to the receive address with index 3.
-    recv_addr = lianad.rpc.listaddresses(3, 1)["addresses"][0]["receive"]
-    res = lianad.rpc.createspend({recv_addr: 11_955_000}, [], 1)
-    assert "psbt" in res
-    # Max(receive_index, change_index) is now 4:
-    assert len(lianad.rpc.listaddresses()["addresses"]) == 4
-    # But the spend has no change:
-    psbt = PSBT.from_base64(res["psbt"])
-    assert len(psbt.o) == 1
-    # As the spend has no change, only the receive index was incremented.
-    # Therefore, the DB receive index is now 4.
-
 
 def test_coin_selection(lianad, bitcoind):
     """We can create a spend using coin selection."""
     # Send to an (external) address.
-    dest_addr_1 = bitcoind.rpc.getnewaddress()
+    dest_100_000 = {bitcoind.rpc.getnewaddress(): 100_000}
     # Coin selection is not possible if we have no coins.
     assert len(lianad.rpc.listcoins()["coins"]) == 0
-    assert "missing" in lianad.rpc.createspend({dest_addr_1: 100_000}, [], 2)
+    assert "missing" in lianad.rpc.createspend(dest_100_000, [], 2)
 
     # Receive a coin in an unconfirmed deposit transaction.
-    recv_addr_1 = lianad.rpc.getnewaddress()["address"]
-    deposit_1 = bitcoind.rpc.sendtoaddress(recv_addr_1, 0.0012)  # 120_000 sats
+    recv_addr = lianad.rpc.getnewaddress()["address"]
+    deposit = bitcoind.rpc.sendtoaddress(recv_addr, 0.0008)  # 80_000 sats
     wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 1)
-    # There are still no confirmed coins or unconfirmed change
-    # to use as candidates for selection.
+    # There are still no confirmed coins to use as candidates for selection.
     assert len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 0
     assert len(lianad.rpc.listcoins(["unconfirmed"])["coins"]) == 1
-    assert lianad.rpc.listcoins(["unconfirmed"])["coins"][0]["is_change"] is False
-    assert "missing" in lianad.rpc.createspend({dest_addr_1: 100_000}, [], 2)
+    assert "missing" in lianad.rpc.createspend(dest_100_000, [], 2)
 
     # Confirm coin.
-    bitcoind.generate_block(1, wait_for_mempool=deposit_1)
+    bitcoind.generate_block(1, wait_for_mempool=deposit)
     wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 1)
-    # Coin selection now succeeds.
-    spend_res_1 = lianad.rpc.createspend({dest_addr_1: 100_000}, [], 2)
-    assert "psbt" in spend_res_1
-    assert len(spend_res_1["warnings"]) == 0
-    # Increase spend amount and we have insufficient funds again even though we
-    # now have confirmed coins.
-    assert "missing" in lianad.rpc.createspend({dest_addr_1: 200_000}, [], 2)
-
-    # The transaction contains a change output.
-    spend_psbt_1 = PSBT.from_base64(spend_res_1["psbt"])
-    assert len(spend_psbt_1.o) == 2
-    assert len(spend_psbt_1.tx.vout) == 2
+
+    # Insufficient funds for coin selection.
+    assert "missing" in lianad.rpc.createspend(dest_100_000, [], 2)
+
+    # Reduce spend amount.
+    dest_30_000 = {bitcoind.rpc.getnewaddress(): 30_000}
+    res = lianad.rpc.createspend(dest_30_000, [], 2)
+    assert "psbt" in res
+
+    # The transaction must contain a change output.
+    spend_psbt = PSBT.from_base64(res["psbt"])
+    assert len(spend_psbt.o) == 2
+    assert len(spend_psbt.tx.vout) == 2
 
     # Sign and broadcast this Spend transaction.
-    spend_txid_1 = sign_and_broadcast_psbt(lianad, spend_psbt_1)
+    signed_psbt = lianad.signer.sign_psbt(spend_psbt)
+    lianad.rpc.updatespend(signed_psbt.to_base64())
+    spend_txid = signed_psbt.tx.txid().hex()
+    lianad.rpc.broadcastspend(spend_txid)
+
     wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == 2)
+    coins = lianad.rpc.listcoins()["coins"]
     # Check that change output is unconfirmed.
     assert len(lianad.rpc.listcoins(["unconfirmed"])["coins"]) == 1
-    assert lianad.rpc.listcoins(["unconfirmed"])["coins"][0]["is_change"] is True
     assert len(lianad.rpc.listcoins(["spending"])["coins"]) == 1
-    # We can use unconfirmed change as candidate.
-    # Depending on the feerate, we'll get a warning about paying extra for the ancestor.
-    dest_addr_2 = bitcoind.rpc.getnewaddress()
-    # If feerate is higher than ancestor, we'll need to pay extra.
-
-    # Try 10 sat/vb:
-    spend_res_2 = lianad.rpc.createspend({dest_addr_2: 10_000}, [], 10)
-    assert "psbt" in spend_res_2
-    spend_psbt_2 = PSBT.from_base64(spend_res_2["psbt"])
-    # The spend is using the unconfirmed change.
-    assert spend_psbt_2.tx.vin[0].prevout.hash == uint256_from_str(
-        bytes.fromhex(spend_txid_1)[::-1]
-    )
-    assert bitcoind.rpc.getmempoolentry(spend_txid_1)["ancestorsize"] == 161
-    assert bitcoind.rpc.getmempoolentry(spend_txid_1)["fees"]["ancestor"] * COIN == 339
-    # ancestor vsize at feerate 10 sat/vb = ancestor_fee / 10 = 339 / 10 = 33
-    # extra_weight <= (extra vsize * witness factor) = (161 - 33) * 4 = 512
-    # additional fee at 10 sat/vb (2.5 sat/wu) = 512 * 2.5 = 1280
-    assert len(spend_res_2["warnings"]) == 1
-    assert (
-        spend_res_2["warnings"][0]
-        == "An additional fee of 1280 sats has been added to pay for ancestors at the target feerate."
-    )
+    # Check we cannot use coins as candidates if they are spending/spent or unconfirmed.
+    assert "missing" in lianad.rpc.createspend(dest_30_000, [], 2)
 
-    # Try 3 sat/vb:
-    spend_res_2 = lianad.rpc.createspend({dest_addr_2: 10_000}, [], 3)
-    assert "psbt" in spend_res_2
-    spend_psbt_2 = PSBT.from_base64(spend_res_2["psbt"])
-    # The spend is using the unconfirmed change.
-    assert spend_psbt_2.tx.vin[0].prevout.hash == uint256_from_str(
-        bytes.fromhex(spend_txid_1)[::-1]
-    )
-    assert bitcoind.rpc.getmempoolentry(spend_txid_1)["ancestorsize"] == 161
-    assert bitcoind.rpc.getmempoolentry(spend_txid_1)["fees"]["ancestor"] * COIN == 339
-    # ancestor vsize at feerate 3 sat/vb = ancestor_fee / 3 = 339 / 3 = 113
-    # extra_weight <= (extra vsize * witness factor) = (161 - 113) * 4 = 192
-    # additional fee at 3 sat/vb (0.75 sat/wu) = 192 * 0.75 = 144
-    assert len(spend_res_2["warnings"]) == 1
-    assert (
-        spend_res_2["warnings"][0]
-        == "An additional fee of 144 sats has been added to pay for ancestors at the target feerate."
-    )
-
-    # 2 sat/vb is same feerate as ancestor and we have no warnings:
-    spend_res_2 = lianad.rpc.createspend({dest_addr_2: 10_000}, [], 2)
-    assert "psbt" in spend_res_2
-    assert len(spend_res_2["warnings"]) == 0
-    spend_psbt_2 = PSBT.from_base64(spend_res_2["psbt"])
-    # The spend is using the unconfirmed change.
-    assert spend_psbt_2.tx.vin[0].prevout.hash == uint256_from_str(
-        bytes.fromhex(spend_txid_1)[::-1]
-    )
+    # Now confirm the Spend.
+    bitcoind.generate_block(1, wait_for_mempool=spend_txid)
+    wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 1)
+    # But its value is not enough for this Spend.
+    dest_60_000 = {bitcoind.rpc.getnewaddress(): 60_000}
+    assert "missing" in lianad.rpc.createspend(dest_60_000, [], 2)
 
     # Get another coin to check coin selection with more than one candidate.
-    recv_addr_2 = lianad.rpc.getnewaddress()["address"]
-    deposit_2 = bitcoind.rpc.sendtoaddress(recv_addr_2, 30_000 / COIN)
-    wait_for(lambda: len(lianad.rpc.listcoins(["unconfirmed"])["coins"]) == 2)
-    assert (
-        len(
-            [
-                c
-                for c in lianad.rpc.listcoins(["unconfirmed"])["coins"]
-                if c["is_change"]
-            ]
-        )
-        == 1
-    )
-    dest_addr_3 = bitcoind.rpc.getnewaddress()
-    # As only one unconfirmed coin is change, we have insufficient funds.
-    assert "missing" in lianad.rpc.createspend({dest_addr_3: 20_000}, [], 10)
-
-    # If we include both unconfirmed coins manually, it will succeed.
-    # We'll need to pay extra for each unconfirmed coin's ancestors.
-    outpoints = [c["outpoint"] for c in lianad.rpc.listcoins(["unconfirmed"])["coins"]]
-
-    spend_res_3 = lianad.rpc.createspend({dest_addr_3: 20_000}, outpoints, 10)
-    assert "psbt" in spend_res_3
-    assert bitcoind.rpc.getmempoolentry(deposit_2)["ancestorsize"] == 165
-    assert bitcoind.rpc.getmempoolentry(deposit_2)["fees"]["ancestor"] * COIN == 165
-    # From above, extra fee for unconfirmed change at 10 sat/vb = 1280.
-    # For unconfirmed non-change:
-    # ancestor vsize at feerate 10 sat/vb = ancestor_fee / 10 = 165 / 10 = 16
-    # extra_weight <= (extra vsize * witness factor) = (165 - 16) * 4 = 596
-    # additional fee at 10 sat/vb (2.5 sat/wu) = 596 * 2.5 = 1490
-    # Sum of extra ancestor fees = 1280 + 1490 = 2770.
-    assert len(spend_res_3["warnings"]) == 1
-    assert (
-        spend_res_3["warnings"][0]
-        == "An additional fee of 2770 sats has been added to pay for ancestors at the target feerate."
-    )
-    spend_psbt_3 = PSBT.from_base64(spend_res_3["psbt"])
-    spend_txid_3 = sign_and_broadcast_psbt(lianad, spend_psbt_3)
-    mempool_txid_3 = bitcoind.rpc.getmempoolentry(spend_txid_3)
-    # The effective feerate of new transaction plus ancestor matches the target.
-    # Note that in the mempool entry, "ancestor" includes spend_txid_3 itself.
-    assert (
-        mempool_txid_3["fees"]["ancestor"] * COIN // mempool_txid_3["ancestorsize"]
-        == 10
-    )
-    # The spend_txid_3 transaction itself has a higher feerate.
-    assert (mempool_txid_3["fees"]["base"] * COIN) // mempool_txid_3["vsize"] > 10
-    # If we subtract the extra that pays for the ancestor, the feerate is at the target value.
-    assert ((mempool_txid_3["fees"]["base"] * COIN) - 2770) // mempool_txid_3[
-        "vsize"
-    ] == 10
-
-    # Now confirm the spend.
-    bitcoind.generate_block(1, wait_for_mempool=spend_txid_3)
-    wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 1)
+    recv_addr = lianad.rpc.getnewaddress()["address"]
+    deposit = bitcoind.rpc.sendtoaddress(recv_addr, 0.0002)  # 20_000 sats
+    bitcoind.generate_block(1, wait_for_mempool=deposit)
+    wait_for(lambda: len(lianad.rpc.listcoins(["confirmed"])["coins"]) == 2)
 
-    # Now create the same spend with auto and manual selection:
-    dest_addr_4 = bitcoind.rpc.getnewaddress()
-    spend_res_4 = lianad.rpc.createspend({dest_addr_4: 15_000}, [], 2)
-    assert "psbt" in spend_res_4
-    assert len(spend_res_4["warnings"]) == 0
+    res = lianad.rpc.createspend(dest_60_000, [], 2)
+    assert "psbt" in res
 
-    # The transaction contains a change output.
-    spend_psbt_4 = PSBT.from_base64(spend_res_4["psbt"])
-    assert len(spend_psbt_4.i) == 1
-    assert len(spend_psbt_4.o) == 2
-    assert len(spend_psbt_4.tx.vout) == 2
+    # The transaction must contain a change output.
+    auto_psbt = PSBT.from_base64(res["psbt"])
+    assert len(auto_psbt.o) == 2
+    assert len(auto_psbt.tx.vout) == 2
 
     # Now create a transaction with manual coin selection using the same outpoints.
     outpoints = [
-        f"{txin.prevout.hash:064x}:{txin.prevout.n}" for txin in spend_psbt_4.tx.vin
+        f"{txin.prevout.hash:064x}:{txin.prevout.n}" for txin in auto_psbt.tx.vin
     ]
-    assert len(outpoints) > 0
-    res_manual = lianad.rpc.createspend({dest_addr_4: 15_000}, outpoints, 2)
-    assert len(res_manual["warnings"]) == 0
-    psbt_manual = PSBT.from_base64(res_manual["psbt"])
+    res_manual = lianad.rpc.createspend(dest_60_000, outpoints, 2)
+    manual_psbt = PSBT.from_base64(res_manual["psbt"])
 
     # Recipient details are the same for both.
-    assert spend_psbt_4.tx.vout[0].nValue == psbt_manual.tx.vout[0].nValue
-    assert spend_psbt_4.tx.vout[0].scriptPubKey == psbt_manual.tx.vout[0].scriptPubKey
+    assert auto_psbt.tx.vout[0].nValue == manual_psbt.tx.vout[0].nValue
+    assert auto_psbt.tx.vout[0].scriptPubKey == manual_psbt.tx.vout[0].scriptPubKey
     # Change amount is the same (change address will be different).
-    assert spend_psbt_4.tx.vout[1].nValue == psbt_manual.tx.vout[1].nValue
-    assert spend_psbt_4.tx.vout[1].scriptPubKey != psbt_manual.tx.vout[1].scriptPubKey
+    assert auto_psbt.tx.vout[1].nValue == manual_psbt.tx.vout[1].nValue
 
 
 def test_coin_selection_changeless(lianad, bitcoind):