Merge #74: Decent change management (using multipath descriptors)

117171f24ff5bd6731d9e5e50e4515a03013a9eb commands: use a separate key chain for change addresses (Antoine Poinsot)
d9f905a19a5c6076683bbe7714ecb86fbafe0555 db: track the next unused derivation index for change, too (Antoine Poinsot)
58a0e57c59bf7b4f451580917f701e95e45d1af6 db: record whether a coin was received on a change address (Antoine Poinsot)
9b04a551474b2cd5ed793e42832454635d964495 db: store derivation index also for addresses from the change desc (Antoine Poinsot)
4f3daa7741b6996c17133dacedb4e70c66f5bac8 descriptors: cache the receive and change descriptors (Antoine Poinsot)
ca3d7c1f3360daa53fbec723fb960051a926d687 descriptors: introduce a newtype for the multipath descriptor (Antoine Poinsot)
1320ee30bacdedd449dfa89a3008e01995917c9f daemon: use multipath descriptors (Antoine Poinsot)
d4db804e4bad928466fa1db4a84a474ff4c33d7b qa: add a missing 'wait_for' in spend creation test (Antoine Poinsot)
7a18c583cbfcd958db9c79e8b13a0a68c1d20d41 bitcoind: filter received coins based on parent descriptors (Antoine Poinsot)
ba4c1e0383e302d2a822176d93b3b9fadc174b4b bitcoind: include change outputs in listsinceblock (Antoine Poinsot)
caaca1fd1a721acae150b8bd9212e51b8e378c99 descriptors: rename derive into derive_received (Antoine Poinsot)
f985fd787917e344b17ae90edd7b99bc1c9f3a7c descriptors: remove as_inner method (Antoine Poinsot)
846d924792089e41e530e414b26f0823afc151eb qa: upgrade python-bip380 to latest master (Antoine Poinsot)
3105b86a28444097e20e14261ffbfd5448b2854b Use my own fork of rust-miniscript (Antoine Poinsot)

Pull request description:

  This fixes #18 by implementing the de-facto standard of using a `/0/*` keychain for receiving addresses and a `/1/*` keychain for change addresses. Note that once we'll have multisig, reusing addresses will still be possible since wallet don't share the same "next derivation index".

  In order to avoid forcing the user to configure and backup two almost identical descriptors, we make use of the recently proposed BIP389 (https://github.com/bitcoin/bips/pull/1354). In order to prevent as much as possible introducing a backward incompatibility in the configuration file after the first release, we restrict the usage of multipath descriptors to `<0;1>` here.
  We now derive public keys from `xpub/0/*` and `xpub/1/*` while we were previously deriving them from `xpub/*`.

  This triggered a pretty invasive refactoring, as most parts of the codebase had to be updated to support the new change/receive separation (even the functional test miniscript dependency had to be updated, see https://github.com/darosior/python-bip380/pull/21).
  Broadly, this:
  1. Update our Miniscript dependency to my upstream PR (https://github.com/rust-bitcoin/rust-miniscript/pull/470) rebased on top of the 8.0.0 release.
  2. Updates the descriptors module to handle somewhat safely the multipath descriptors (to avoid mixing up the single, multi, and derived descriptors).
  3. Makes a multipath descriptor mandatory in the configuration file.
  4. Updates the Bitcoin backend poller aware of descriptors for which to track coins.
     - Necessarily this updates the bitcoind implementation to import two descriptors
  5. Record in database whether a coin was for the change or receive descriptor, in addition to its derivation index

ACKs for top commit:
  edouardparis:
    ACK 117171f24ff5bd6731d9e5e50e4515a03013a9eb

Tree-SHA512: efcb7267f1ba6a5a3072e96fd1c70272f81092e86ee1178833f83d0aa88f271f42c269b71ca9992e76bb3e103baf1a189a609cc20f14f29b7388ab133da99044
This commit is contained in:
Antoine Poinsot 2022-10-28 13:44:25 +02:00
commit 8b129fe3e5
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
20 changed files with 670 additions and 251 deletions

3
Cargo.lock generated
View File

@ -256,8 +256,7 @@ dependencies = [
[[package]]
name = "miniscript"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f4975078076f0b7b914a3044ad7432d2a7fcec38edb855afdc672e24ca35b69"
source = "git+https://github.com/darosior/rust-miniscript?branch=multipath_descriptors_on_8.0#7d756f2ab066d85d299f711f953ebda15f14e832"
dependencies = [
"bitcoin",
"serde",

View File

@ -25,7 +25,8 @@ jsonrpc_server = []
[dependencies]
# For managing transactions (it re-exports the bitcoin crate)
miniscript = { version = "8.0", features = ["serde"] }
# TODO: don't use my fork for a real release..
miniscript = { git = "https://github.com/darosior/rust-miniscript", branch = "multipath_descriptors_on_8.0", features = ["serde"] }
# Don't reinvent the wheel
dirs = "3.0"

View File

@ -1,7 +1,7 @@
///! Implementation of the Bitcoin interface using bitcoind.
///!
///! We use the RPC interface and a watchonly descriptor wallet.
use crate::{bitcoin::BlockChainTip, config, descriptors::InheritanceDescriptor};
use crate::{bitcoin::BlockChainTip, config, descriptors::MultipathDescriptor};
use std::{collections::HashSet, convert::TryInto, fs, io, str::FromStr, time::Duration};
@ -10,7 +10,7 @@ use jsonrpc::{
client::Client,
simple_http::{self, SimpleHttpTransport},
};
use miniscript::bitcoin;
use miniscript::{bitcoin, descriptor};
use serde_json::Value as Json;
@ -351,13 +351,19 @@ impl BitcoinD {
None
}
// TODO: rescan feature will probably need another timestamp than 'now'
fn import_descriptor(&self, descriptor: &InheritanceDescriptor) -> Option<String> {
let descriptors = vec![serde_json::json!({
"desc": descriptor.to_string(),
"timestamp": "now",
"active": false,
})];
// Import the receive and change descriptors from the multipath descriptor to bitcoind.
fn import_descriptor(&self, desc: &MultipathDescriptor) -> Option<String> {
let descriptors = [desc.receive_descriptor(), desc.change_descriptor()]
.iter()
.map(|desc| {
// TODO: rescan feature will probably need another timestamp than 'now'
serde_json::json!({
"desc": desc.to_string(),
"timestamp": "now",
"active": false,
})
})
.collect();
let res = self.make_wallet_request("importdescriptors", &params!(Json::Array(descriptors)));
let all_succeeded = res
@ -395,7 +401,7 @@ impl BitcoinD {
/// Create the watchonly wallet on bitcoind, and import it the main descriptor.
pub fn create_watchonly_wallet(
&self,
main_descriptor: &InheritanceDescriptor,
main_descriptor: &MultipathDescriptor,
) -> Result<(), BitcoindError> {
// Remove any leftover. This can happen if we delete the watchonly wallet but don't restart
// bitcoind.
@ -435,7 +441,7 @@ impl BitcoinD {
/// Perform various sanity checks on the bitcoind instance.
pub fn sanity_check(
&self,
main_descriptor: &InheritanceDescriptor,
main_descriptor: &MultipathDescriptor,
config_network: bitcoin::Network,
) -> Result<(), BitcoindError> {
// Check the minimum supported bitcoind version
@ -471,9 +477,11 @@ impl BitcoinD {
}
// Check our main descriptor is imported in this wallet.
if !self
.list_descriptors()
.contains(&main_descriptor.to_string())
let receive_desc = main_descriptor.receive_descriptor();
let change_desc = main_descriptor.change_descriptor();
let desc_list = self.list_descriptors();
if !desc_list.contains(&receive_desc.to_string())
|| !desc_list.contains(&change_desc.to_string())
{
return Err(BitcoindError::MissingDescriptor);
}
@ -528,7 +536,13 @@ impl BitcoinD {
pub fn list_since_block(&self, block_hash: &bitcoin::BlockHash) -> LSBlockRes {
self.make_wallet_request(
"listsinceblock",
&params!(Json::String(block_hash.to_string()),),
&params!(
Json::String(block_hash.to_string()),
Json::Number(1.into()), // Default for min_confirmations for the returned
Json::Bool(true), // Whether to include watchonly
Json::Bool(false), // Whether to include an array of txs that were removed in reorgs
Json::Bool(true) // Whether to include UTxOs treated as change.
),
)
.into()
}
@ -708,6 +722,7 @@ pub struct LSBlockEntry {
pub amount: bitcoin::Amount,
pub block_height: Option<i32>,
pub address: bitcoin::Address,
pub parent_descs: Vec<descriptor::Descriptor<descriptor::DescriptorPublicKey>>,
}
impl From<&Json> for LSBlockEntry {
@ -739,12 +754,26 @@ impl From<&Json> for LSBlockEntry {
.and_then(Json::as_str)
.and_then(|s| bitcoin::Address::from_str(s).ok())
.expect("bitcoind can't give a bad address");
let parent_descs = json
.get("parent_descs")
.and_then(Json::as_array)
.and_then(|descs| {
descs
.iter()
.map(|desc| {
desc.as_str()
.and_then(|s| descriptor::Descriptor::<_>::from_str(s).ok())
})
.collect::<Option<Vec<_>>>()
})
.expect("bitcoind can't give invalid descriptors");
LSBlockEntry {
outpoint,
amount,
block_height,
address,
parent_descs,
}
}
}

View File

@ -4,7 +4,10 @@
pub mod d;
pub mod poller;
use d::{BitcoindError, LSBlockEntry};
use crate::{
bitcoin::d::{BitcoindError, LSBlockEntry},
descriptors,
};
use std::{collections::HashMap, error, fmt, sync};
@ -56,7 +59,11 @@ pub trait BitcoinInterface: Send {
fn is_in_chain(&self, tip: &BlockChainTip) -> bool;
/// Get coins received since the specified tip.
fn received_coins(&self, tip: &BlockChainTip) -> Vec<UTxO>;
fn received_coins(
&self,
tip: &BlockChainTip,
descs: &[descriptors::InheritanceDescriptor],
) -> Vec<UTxO>;
/// Get all coins that were confirmed, and at what height and time.
fn confirmed_coins(
@ -106,25 +113,37 @@ impl BitcoinInterface for d::BitcoinD {
.unwrap_or(false)
}
fn received_coins(&self, tip: &BlockChainTip) -> Vec<UTxO> {
fn received_coins(
&self,
tip: &BlockChainTip,
descs: &[descriptors::InheritanceDescriptor],
) -> Vec<UTxO> {
// TODO: don't assume only a single descriptor is loaded on the wo wallet
let lsb_res = self.list_since_block(&tip.hash);
lsb_res
.received_coins
.into_iter()
.map(|entry| {
.filter_map(|entry| {
let LSBlockEntry {
outpoint,
amount,
block_height,
address,
parent_descs,
} = entry;
UTxO {
outpoint,
amount,
block_height,
address,
if parent_descs
.iter()
.any(|parent_desc| descs.iter().any(|desc| desc == parent_desc))
{
Some(UTxO {
outpoint,
amount,
block_height,
address,
})
} else {
None
}
})
.collect()
@ -287,8 +306,12 @@ impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>>
self.lock().unwrap().is_in_chain(tip)
}
fn received_coins(&self, tip: &BlockChainTip) -> Vec<UTxO> {
self.lock().unwrap().received_coins(tip)
fn received_coins(
&self,
tip: &BlockChainTip,
descs: &[descriptors::InheritanceDescriptor],
) -> Vec<UTxO> {
self.lock().unwrap().received_coins(tip, descs)
}
fn confirmed_coins(

View File

@ -1,6 +1,7 @@
use crate::{
bitcoin::{BitcoinInterface, BlockChainTip, UTxO},
database::{Coin, DatabaseConnection, DatabaseInterface},
descriptors,
};
use std::{
@ -26,14 +27,17 @@ fn update_coins(
bit: &impl BitcoinInterface,
db_conn: &mut Box<dyn DatabaseConnection>,
previous_tip: &BlockChainTip,
descs: &[descriptors::InheritanceDescriptor],
) -> UpdatedCoins {
let curr_coins = db_conn.coins();
log::debug!("Current coins: {:?}", curr_coins);
// Start by fetching newly received coins.
let mut received = Vec::new();
for utxo in bit.received_coins(previous_tip) {
if let Some(derivation_index) = db_conn.derivation_index_by_address(&utxo.address) {
for utxo in bit.received_coins(previous_tip, descs) {
if let Some((derivation_index, is_change)) =
db_conn.derivation_index_by_address(&utxo.address)
{
if !curr_coins.contains_key(&utxo.outpoint) {
let UTxO {
outpoint, amount, ..
@ -42,6 +46,7 @@ fn update_coins(
outpoint,
amount,
derivation_index,
is_change,
block_height: None,
block_time: None,
spend_txid: None,
@ -154,7 +159,11 @@ fn new_tip(bit: &impl BitcoinInterface, current_tip: &BlockChainTip) -> TipUpdat
TipUpdate::Reorged(common_ancestor)
}
fn updates(bit: &impl BitcoinInterface, db: &impl DatabaseInterface) {
fn updates(
bit: &impl BitcoinInterface,
db: &impl DatabaseInterface,
descs: &[descriptors::InheritanceDescriptor],
) {
let mut db_conn = db.connection();
// Check if there was a new block before updating ourselves.
@ -167,18 +176,18 @@ fn updates(bit: &impl BitcoinInterface, db: &impl DatabaseInterface) {
// between our former chain and the new one, then restart fresh.
db_conn.rollback_tip(&new_tip);
log::info!("Tip was rolled back to '{}'.", new_tip);
return updates(bit, db);
return updates(bit, db, descs);
}
};
// Then check the state of our coins. Do it even if the tip did not change since last poll, as
// we may have unconfirmed transactions.
let updated_coins = update_coins(bit, &mut db_conn, &current_tip);
let updated_coins = update_coins(bit, &mut db_conn, &current_tip, descs);
// If the tip changed while we were polling our Bitcoin interface, start over.
if bit.chain_tip() != latest_tip {
log::info!("Chain tip changed while we were updating our state. Starting over.");
return updates(bit, db);
return updates(bit, db, descs);
}
// The chain tip did not change since we started our updates. Record them and the latest tip.
@ -213,9 +222,14 @@ pub fn looper(
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
shutdown: sync::Arc<atomic::AtomicBool>,
poll_interval: time::Duration,
desc: descriptors::MultipathDescriptor,
) {
let mut last_poll = None;
let mut synced = false;
let descs = [
desc.receive_descriptor().clone(),
desc.change_descriptor().clone(),
];
maybe_initialize_tip(&bit, &db);
@ -247,6 +261,6 @@ pub fn looper(
}
}
updates(&bit, &db);
updates(&bit, &db, &descs);
}
}

View File

@ -3,6 +3,7 @@ mod looper;
use crate::{
bitcoin::{poller::looper::looper, BitcoinInterface},
database::DatabaseInterface,
descriptors,
};
use std::{
@ -21,13 +22,14 @@ impl Poller {
bit: sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
poll_interval: time::Duration,
desc: descriptors::MultipathDescriptor,
) -> Poller {
let shutdown = sync::Arc::from(atomic::AtomicBool::from(false));
let handle = thread::Builder::new()
.name("Bitcoin poller".to_string())
.spawn({
let shutdown = shutdown.clone();
move || looper(bit, db, shutdown, poll_interval)
move || looper(bit, db, shutdown, poll_interval, desc)
})
.expect("Must not fail");

View File

@ -20,7 +20,6 @@ use std::{
use miniscript::{
bitcoin::{
self,
util::bip32,
util::psbt::{Input as PsbtIn, Output as PsbtOut, PartiallySignedTransaction as Psbt},
},
psbt::PsbtExt,
@ -170,9 +169,14 @@ fn serializable_size<T: bitcoin::consensus::Encodable + ?Sized>(t: &T) -> u64 {
}
impl DaemonControl {
// Get the descriptor at this derivation index
fn derived_desc(&self, index: bip32::ChildNumber) -> descriptors::DerivedInheritanceDescriptor {
self.config.main_descriptor.derive(index, &self.secp)
// Get the derived descriptor for this coin
fn derived_desc(&self, coin: &Coin) -> descriptors::DerivedInheritanceDescriptor {
let desc = if coin.is_change {
self.config.main_descriptor.change_descriptor()
} else {
self.config.main_descriptor.receive_descriptor()
};
desc.derive(coin.derivation_index, &self.secp)
}
}
@ -197,12 +201,13 @@ impl DaemonControl {
/// whether it was actually used.
pub fn get_new_address(&self) -> GetAddressResult {
let mut db_conn = self.db.connection();
let index = db_conn.derivation_index();
let index = db_conn.receive_index();
// TODO: should we wrap around instead of failing?
db_conn.increment_derivation_index(&self.secp);
db_conn.increment_receive_index(&self.secp);
let address = self
.config
.main_descriptor
.receive_descriptor()
.derive(index, &self.secp)
.address(self.config.bitcoin_config.network);
GetAddressResult { address }
@ -278,7 +283,7 @@ impl DaemonControl {
..bitcoin::TxIn::default()
});
let coin_desc = self.derived_desc(coin.derivation_index);
let coin_desc = self.derived_desc(coin);
sat_vb += desc_sat_vb(&coin_desc);
let witness_script = Some(coin_desc.witness_script());
let witness_utxo = Some(bitcoin::TxOut {
@ -340,14 +345,15 @@ impl DaemonControl {
// an added output* (for the change).
if nochange_feerate_vb > feerate_vb {
// Get the change address to create a dummy change txo.
// TODO: decent change management
let first_coin = coins
.get(coins_outpoints.get(0).expect("We checked it wasn't empty"))
.expect("We checked they were all present");
let coin_desc = self.derived_desc(first_coin.derivation_index);
let change_desc = self
.config
.main_descriptor
.receive_descriptor()
.derive(db_conn.change_index(), &self.secp);
db_conn.increment_change_index(&self.secp);
let mut change_txo = bitcoin::TxOut {
value: std::u64::MAX,
script_pubkey: coin_desc.script_pubkey(),
script_pubkey: change_desc.script_pubkey(),
};
// Serialized size is equal to the virtual size for an output.
let change_vb: u64 = serializable_size(&change_txo);
@ -487,7 +493,7 @@ impl DaemonControl {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetInfoDescriptors {
pub main: descriptors::InheritanceDescriptor,
pub main: descriptors::MultipathDescriptor,
}
/// Information about the daemon
@ -554,6 +560,8 @@ mod tests {
use crate::testutils::*;
use std::str::FromStr;
use bitcoin::util::bip32;
#[test]
fn getinfo() {
let ms = DummyMinisafe::new();
@ -572,7 +580,7 @@ mod tests {
assert_eq!(
addr,
bitcoin::Address::from_str(
"bc1qgudekhcrejgtlx3yhlvdul7t4q76e5lhm0vtcsndxs6aslh4r9jsqkqhwu"
"bc1q9ksrc647hx8zp2cewl8p5f487dgux3777yees8rjcx46t4daqzzqt7yga8"
)
.unwrap()
);
@ -626,6 +634,7 @@ mod tests {
block_time: None,
amount: bitcoin::Amount::from_sat(100_000),
derivation_index: bip32::ChildNumber::from(13),
is_change: false,
spend_txid: None,
spend_block: None,
}]);
@ -720,6 +729,7 @@ mod tests {
block_time: None,
amount: bitcoin::Amount::from_sat(100_000),
derivation_index: bip32::ChildNumber::from(13),
is_change: false,
spend_txid: None,
spend_block: None,
},
@ -729,6 +739,7 @@ mod tests {
block_time: None,
amount: bitcoin::Amount::from_sat(115_680),
derivation_index: bip32::ChildNumber::from(34),
is_change: false,
spend_txid: None,
spend_block: None,
},

View File

@ -1,8 +1,8 @@
use crate::descriptors::InheritanceDescriptor;
use crate::descriptors::MultipathDescriptor;
use std::{net::SocketAddr, path::PathBuf, str::FromStr, time::Duration};
use miniscript::{bitcoin::Network, DescriptorPublicKey, ForEachKey};
use miniscript::bitcoin::Network;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
@ -92,7 +92,7 @@ pub struct Config {
deserialize_with = "deserialize_fromstr",
serialize_with = "serialize_to_string"
)]
pub main_descriptor: InheritanceDescriptor,
pub main_descriptor: MultipathDescriptor,
/// Settings for the Bitcoin interface
pub bitcoin_config: BitcoinConfig,
/// Settings specific to bitcoind as the Bitcoin interface
@ -113,7 +113,7 @@ pub enum ConfigError {
DatadirNotFound,
FileNotFound,
ReadingFile(String),
UnexpectedDescriptor(Box<InheritanceDescriptor>),
UnexpectedDescriptor(Box<MultipathDescriptor>),
Unexpected(String),
}
@ -203,14 +203,7 @@ impl Config {
Network::Bitcoin => Network::Bitcoin,
_ => Network::Testnet,
};
let unexpected_net = self.main_descriptor.as_inner().for_each_key(|xpub| {
if let DescriptorPublicKey::XPub(xpub) = xpub {
xpub.xkey.network != expected_network
} else {
false
}
});
if unexpected_net {
if !self.main_descriptor.all_xpubs_net_is(expected_network) {
return Err(ConfigError::Unexpected(format!(
"Our bitcoin network is {} but one xpub is not for network {}",
self.bitcoin_config.network, expected_network
@ -235,7 +228,7 @@ mod tests {
data_dir = "/home/wizardsardine/custom/folder/"
daemon = false
log_level = "debug"
main_descriptor = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2d"
main_descriptor = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#5f6qd0d9"
[bitcoin_config]
network = "bitcoin"
@ -252,7 +245,7 @@ mod tests {
data_dir = '/home/wizardsardine/custom/folder/'
daemon = false
log_level = 'TRACE'
main_descriptor = 'wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2d'
main_descriptor = 'wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#5f6qd0d9'
[bitcoin_config]
network = 'bitcoin'
@ -273,7 +266,7 @@ mod tests {
log_level = "trace"
data_dir = "/home/wizardsardine/custom/folder/"
main_descriptor = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2e"
main_descriptor = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#y5wcna2e"
[bitcoin_config]
network = "bitcoin"

View File

@ -45,14 +45,19 @@ pub trait DatabaseConnection {
/// Update our best chain seen.
fn update_tip(&mut self, tip: &BlockChainTip);
fn derivation_index(&mut self) -> bip32::ChildNumber;
fn receive_index(&mut self) -> bip32::ChildNumber;
fn increment_derivation_index(&mut self, secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>);
fn change_index(&mut self) -> bip32::ChildNumber;
fn increment_receive_index(&mut self, secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>);
fn increment_change_index(&mut self, secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>);
/// Get the derivation index for this address, as well as whether this address is change.
fn derivation_index_by_address(
&mut self,
address: &bitcoin::Address,
) -> Option<bip32::ChildNumber>;
) -> Option<(bip32::ChildNumber, bool)>;
/// Get all our coins, past or present, spent or not.
fn coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin>;
@ -113,12 +118,20 @@ impl DatabaseConnection for SqliteConn {
self.update_tip(tip)
}
fn derivation_index(&mut self) -> bip32::ChildNumber {
fn receive_index(&mut self) -> bip32::ChildNumber {
self.db_wallet().deposit_derivation_index
}
fn increment_derivation_index(&mut self, secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>) {
self.increment_derivation_index(secp)
fn change_index(&mut self) -> bip32::ChildNumber {
self.db_wallet().change_derivation_index
}
fn increment_receive_index(&mut self, secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>) {
self.increment_deposit_index(secp)
}
fn increment_change_index(&mut self, secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>) {
self.increment_change_index(secp)
}
fn coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
@ -154,9 +167,9 @@ impl DatabaseConnection for SqliteConn {
fn derivation_index_by_address(
&mut self,
address: &bitcoin::Address,
) -> Option<bip32::ChildNumber> {
) -> Option<(bip32::ChildNumber, bool)> {
self.db_address(address)
.map(|db_addr| db_addr.derivation_index)
.map(|db_addr| (db_addr.derivation_index, address == &db_addr.change_address))
}
fn coins_by_outpoints(
@ -215,6 +228,7 @@ pub struct Coin {
pub block_time: Option<u32>,
pub amount: bitcoin::Amount,
pub derivation_index: bip32::ChildNumber,
pub is_change: bool,
pub spend_txid: Option<bitcoin::Txid>,
pub spend_block: Option<SpendBlock>,
}
@ -227,6 +241,7 @@ impl std::convert::From<DbCoin> for Coin {
block_time,
amount,
derivation_index,
is_change,
spend_txid,
spend_block,
..
@ -237,6 +252,7 @@ impl std::convert::From<DbCoin> for Coin {
block_time,
amount,
derivation_index,
is_change,
spend_txid,
spend_block: spend_block.map(SpendBlock::from),
}

View File

@ -14,11 +14,11 @@ use crate::{
database::{
sqlite::{
schema::{DbAddress, DbCoin, DbSpendTransaction, DbTip, DbWallet},
utils::{create_fresh_db, db_exec, db_query, db_tx_query, LOOK_AHEAD_LIMIT},
utils::{create_fresh_db, db_exec, db_query, db_tx_query, populate_address_mapping},
},
Coin,
},
descriptors::InheritanceDescriptor,
descriptors::MultipathDescriptor,
};
use std::{convert::TryInto, fmt, io, path};
@ -36,7 +36,7 @@ pub enum SqliteDbError {
FileNotFound(path::PathBuf),
UnsupportedVersion(i64),
InvalidNetwork(bitcoin::Network),
DescriptorMismatch(Box<InheritanceDescriptor>),
DescriptorMismatch(Box<MultipathDescriptor>),
Rusqlite(rusqlite::Error),
}
@ -80,7 +80,7 @@ impl From<rusqlite::Error> for SqliteDbError {
#[derive(Debug, Clone)]
pub struct FreshDbOptions {
pub bitcoind_network: bitcoin::Network,
pub main_descriptor: InheritanceDescriptor,
pub main_descriptor: MultipathDescriptor,
}
#[derive(Debug, Clone)]
@ -119,7 +119,7 @@ impl SqliteDb {
pub fn sanity_check(
&self,
bitcoind_network: bitcoin::Network,
main_descriptor: &InheritanceDescriptor,
main_descriptor: &MultipathDescriptor,
) -> Result<(), SqliteDbError> {
let mut conn = self.connection()?;
@ -210,10 +210,7 @@ impl SqliteConn {
.expect("Database must be available")
}
pub fn increment_derivation_index(
&mut self,
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
) {
pub fn increment_deposit_index(&mut self, secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>) {
let network = self.db_tip().network;
db_exec(&mut self.conn, |db_tx| {
@ -235,19 +232,42 @@ impl SqliteConn {
rusqlite::params![next_index],
)?;
// Update the address to derivation index mapping.
// TODO: have this as a helper in descriptors.rs
let next_la_index = next_index + LOOK_AHEAD_LIMIT - 1;
let next_la_address = db_wallet
.main_descriptor
.derive(next_la_index.into(), secp)
.address(network);
db_tx
.execute(
"INSERT INTO addresses (address, derivation_index) VALUES (?1, ?2)",
rusqlite::params![next_la_address.to_string(), next_la_index],
)
.map(|_| ())
if next_index > db_wallet.change_derivation_index.into() {
populate_address_mapping(db_tx, &db_wallet, next_index, network, secp)?;
}
Ok(())
})
.expect("Database must be available")
}
pub fn increment_change_index(&mut self, secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>) {
let network = self.db_tip().network;
db_exec(&mut self.conn, |db_tx| {
let db_wallet: DbWallet =
db_tx_query(db_tx, "SELECT * FROM wallets", rusqlite::params![], |row| {
row.try_into()
})
.expect("Db must not fail")
.pop()
.expect("There is always a row in the wallet table");
let next_index: u32 = db_wallet
.change_derivation_index
.increment()
.expect("Must not get in hardened territory")
.into();
// NOTE: should be updated if we ever have multi-wallet support
db_tx.execute(
"UPDATE wallets SET change_derivation_index = (?1)",
rusqlite::params![next_index],
)?;
if next_index > db_wallet.deposit_derivation_index.into() {
populate_address_mapping(db_tx, &db_wallet, next_index, network, secp)?;
}
Ok(())
})
.expect("Database must be available")
}
@ -282,14 +302,15 @@ impl SqliteConn {
for coin in coins {
let deriv_index: u32 = coin.derivation_index.into();
db_tx.execute(
"INSERT INTO coins (wallet_id, txid, vout, amount_sat, derivation_index) \
VALUES (?1, ?2, ?3, ?4, ?5)",
"INSERT INTO coins (wallet_id, txid, vout, amount_sat, derivation_index, is_change) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
WALLET_ID,
coin.outpoint.txid.to_vec(),
coin.outpoint.vout,
coin.amount.to_sat(),
deriv_index,
coin.is_change,
],
)?;
}
@ -362,7 +383,7 @@ impl SqliteConn {
pub fn db_address(&mut self, address: &bitcoin::Address) -> Option<DbAddress> {
db_query(
&mut self.conn,
"SELECT * FROM addresses WHERE address = ?1",
"SELECT * FROM addresses WHERE receive_address = ?1 OR change_address = ?1",
rusqlite::params![address.to_string()],
|row| row.try_into(),
)
@ -484,8 +505,8 @@ mod tests {
use bitcoin::{hashes::Hash, util::bip32};
fn dummy_options() -> FreshDbOptions {
let desc_str = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2d";
let main_descriptor = InheritanceDescriptor::from_str(desc_str).unwrap();
let desc_str = "wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#5f6qd0d9";
let main_descriptor = MultipathDescriptor::from_str(desc_str).unwrap();
FreshDbOptions {
bitcoind_network: bitcoin::Network::Bitcoin,
main_descriptor,
@ -533,8 +554,8 @@ mod tests {
.to_string()
.contains("Database was created for network");
fs::remove_file(&db_path).unwrap();
let other_desc_str = "wsh(andor(pk(tpubDExU4YLJkyQ9RRbVScQq2brFxWWha7WmAUByPWyaWYwmcTv3Shx8aHp6mVwuE5n4TeM4z5DTWGf2YhNPmXtfvyr8cUDVvA3txdrFnFgNdF7/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))";
let other_desc = InheritanceDescriptor::from_str(other_desc_str).unwrap();
let other_desc_str = "wsh(andor(pk(tpubDExU4YLJkyQ9RRbVScQq2brFxWWha7WmAUByPWyaWYwmcTv3Shx8aHp6mVwuE5n4TeM4z5DTWGf2YhNPmXtfvyr8cUDVvA3txdrFnFgNdF7/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))";
let other_desc = MultipathDescriptor::from_str(other_desc_str).unwrap();
let db = SqliteDb::new(db_path.clone(), Some(options.clone()), &secp).unwrap();
db.sanity_check(bitcoin::Network::Bitcoin, &other_desc)
.unwrap_err()
@ -601,6 +622,7 @@ mod tests {
block_time: None,
amount: bitcoin::Amount::from_sat(98765),
derivation_index: bip32::ChildNumber::from_normal_idx(10).unwrap(),
is_change: false,
spend_txid: None,
spend_block: None,
};
@ -612,7 +634,7 @@ mod tests {
assert_eq!(coins.len(), 1);
assert_eq!(coins[0].outpoint, coin_a.outpoint);
// Add a second one, we'll get both.
// Add a second one (this one is change), we'll get both.
let coin_b = Coin {
outpoint: bitcoin::OutPoint::from_str(
"61db3e276b095e5b05f1849dd6bfffb4e7e5ec1c4a4210099b98fce01571936f:12",
@ -622,6 +644,7 @@ mod tests {
block_time: None,
amount: bitcoin::Amount::from_sat(1111),
derivation_index: bip32::ChildNumber::from_normal_idx(103).unwrap(),
is_change: true,
spend_txid: None,
spend_block: None,
};
@ -714,6 +737,16 @@ mod tests {
// There is the index for the first index
let addr = options
.main_descriptor
.receive_descriptor()
.derive(0.into(), &secp)
.address(options.bitcoind_network);
let db_addr = conn.db_address(&addr).unwrap();
assert_eq!(db_addr.derivation_index, 0.into());
// And also for the change address
let addr = options
.main_descriptor
.change_descriptor()
.derive(0.into(), &secp)
.address(options.bitcoind_network);
let db_addr = conn.db_address(&addr).unwrap();
@ -722,6 +755,7 @@ mod tests {
// There is the index for the 199th index (look-ahead limit)
let addr = options
.main_descriptor
.receive_descriptor()
.derive(199.into(), &secp)
.address(options.bitcoind_network);
let db_addr = conn.db_address(&addr).unwrap();
@ -730,14 +764,48 @@ mod tests {
// And not for the 200th one.
let addr = options
.main_descriptor
.receive_descriptor()
.derive(200.into(), &secp)
.address(options.bitcoind_network);
assert!(conn.db_address(&addr).is_none());
// But if we increment the deposit derivation index, the 200th one will be there.
conn.increment_derivation_index(&secp);
conn.increment_deposit_index(&secp);
let db_addr = conn.db_address(&addr).unwrap();
assert_eq!(db_addr.derivation_index, 200.into());
// It will also be there for the change descriptor.
let addr = options
.main_descriptor
.change_descriptor()
.derive(200.into(), &secp)
.address(options.bitcoind_network);
let db_addr = conn.db_address(&addr).unwrap();
assert_eq!(db_addr.derivation_index, 200.into());
// But not for the 201th.
let addr = options
.main_descriptor
.change_descriptor()
.derive(201.into(), &secp)
.address(options.bitcoind_network);
assert!(conn.db_address(&addr).is_none());
// If we increment the *change* derivation index to 1, it will still not be there.
conn.increment_change_index(&secp);
assert!(conn.db_address(&addr).is_none());
// But doing it once again it will be there for both change and receive.
conn.increment_change_index(&secp);
let db_addr = conn.db_address(&addr).unwrap();
assert_eq!(db_addr.derivation_index, 201.into());
let addr = options
.main_descriptor
.receive_descriptor()
.derive(201.into(), &secp)
.address(options.bitcoind_network);
let db_addr = conn.db_address(&addr).unwrap();
assert_eq!(db_addr.derivation_index, 201.into());
}
fs::remove_dir_all(&tmp_dir).unwrap();
@ -775,6 +843,7 @@ mod tests {
block_time: None,
amount: bitcoin::Amount::from_sat(98765),
derivation_index: bip32::ChildNumber::from_normal_idx(10).unwrap(),
is_change: false,
spend_txid: None,
spend_block: None,
},
@ -787,6 +856,7 @@ mod tests {
block_time: Some(1_111_899),
amount: bitcoin::Amount::from_sat(98765),
derivation_index: bip32::ChildNumber::from_normal_idx(100).unwrap(),
is_change: false,
spend_txid: None,
spend_block: None,
},
@ -799,6 +869,7 @@ mod tests {
block_time: Some(1_121_899),
amount: bitcoin::Amount::from_sat(98765),
derivation_index: bip32::ChildNumber::from_normal_idx(1000).unwrap(),
is_change: false,
spend_txid: Some(
bitcoin::Txid::from_str(
"0c62a990d20d54429e70859292e82374ba6b1b951a3ab60f26bb65fee5724ff7",
@ -819,6 +890,7 @@ mod tests {
block_time: Some(1_131_899),
amount: bitcoin::Amount::from_sat(98765),
derivation_index: bip32::ChildNumber::from_normal_idx(10000).unwrap(),
is_change: false,
spend_txid: None,
spend_block: None,
},
@ -831,6 +903,7 @@ mod tests {
block_time: Some(1_134_899),
amount: bitcoin::Amount::from_sat(98765),
derivation_index: bip32::ChildNumber::from_normal_idx(100000).unwrap(),
is_change: false,
spend_txid: Some(
bitcoin::Txid::from_str(
"7477017f992cdc7ba08acafb77cb3b5bc0f42ac340d3e1e1da0785bdda20d5f6",

View File

@ -1,4 +1,4 @@
use crate::descriptors::InheritanceDescriptor;
use crate::descriptors::MultipathDescriptor;
use std::{convert::TryFrom, str::FromStr};
@ -27,7 +27,8 @@ CREATE TABLE wallets (
id INTEGER PRIMARY KEY NOT NULL,
timestamp INTEGER NOT NULL,
main_descriptor TEXT NOT NULL,
deposit_derivation_index INTEGER NOT NULL
deposit_derivation_index INTEGER NOT NULL,
change_derivation_index INTEGER NOT NULL
);
/* Our (U)TxOs.
@ -44,6 +45,7 @@ CREATE TABLE coins (
vout INTEGER NOT NULL,
amount_sat INTEGER NOT NULL,
derivation_index INTEGER NOT NULL,
is_change BOOLEAN NOT NULL CHECK (is_change IN (0,1)),
spend_txid BLOB,
spend_block_height INTEGER,
spend_block_time INTEGER,
@ -57,7 +59,8 @@ CREATE TABLE coins (
* we can get the derivation index from the parent descriptor from bitcoind.
*/
CREATE TABLE addresses (
address TEXT NOT NULL UNIQUE,
receive_address TEXT NOT NULL UNIQUE,
change_address TEXT NOT NULL UNIQUE,
derivation_index INTEGER NOT NULL UNIQUE
);
@ -103,8 +106,9 @@ impl TryFrom<&rusqlite::Row<'_>> for DbTip {
pub struct DbWallet {
pub id: i64,
pub timestamp: u32,
pub main_descriptor: InheritanceDescriptor,
pub main_descriptor: MultipathDescriptor,
pub deposit_derivation_index: bip32::ChildNumber,
pub change_derivation_index: bip32::ChildNumber,
}
impl TryFrom<&rusqlite::Row<'_>> for DbWallet {
@ -115,17 +119,20 @@ impl TryFrom<&rusqlite::Row<'_>> for DbWallet {
let timestamp = row.get(1)?;
let desc_str: String = row.get(2)?;
let main_descriptor = InheritanceDescriptor::from_str(&desc_str)
let main_descriptor = MultipathDescriptor::from_str(&desc_str)
.expect("Insane database: can't parse deposit descriptor");
let der_idx: u32 = row.get(3)?;
let deposit_derivation_index = bip32::ChildNumber::from(der_idx);
let der_idx: u32 = row.get(4)?;
let change_derivation_index = bip32::ChildNumber::from(der_idx);
Ok(DbWallet {
id,
timestamp,
main_descriptor,
deposit_derivation_index,
change_derivation_index,
})
}
}
@ -145,6 +152,7 @@ pub struct DbCoin {
pub block_time: Option<u32>,
pub amount: bitcoin::Amount,
pub derivation_index: bip32::ChildNumber,
pub is_change: bool,
pub spend_txid: Option<bitcoin::Txid>,
pub spend_block: Option<DbSpendBlock>,
}
@ -167,12 +175,13 @@ impl TryFrom<&rusqlite::Row<'_>> for DbCoin {
let amount = bitcoin::Amount::from_sat(amount);
let der_idx: u32 = row.get(7)?;
let derivation_index = bip32::ChildNumber::from(der_idx);
let is_change: bool = row.get(8)?;
let spend_txid: Option<Vec<u8>> = row.get(8)?;
let spend_txid: Option<Vec<u8>> = row.get(9)?;
let spend_txid =
spend_txid.map(|txid| encode::deserialize(&txid).expect("We only store valid txids"));
let spend_height: Option<i32> = row.get(9)?;
let spend_time: Option<u32> = row.get(10)?;
let spend_height: Option<i32> = row.get(10)?;
let spend_time: Option<u32> = row.get(11)?;
assert_eq!(spend_height.is_none(), spend_time.is_none());
let spend_block = spend_height.map(|height| DbSpendBlock {
height,
@ -187,6 +196,7 @@ impl TryFrom<&rusqlite::Row<'_>> for DbCoin {
block_time,
amount,
derivation_index,
is_change,
spend_txid,
spend_block,
})
@ -195,7 +205,8 @@ impl TryFrom<&rusqlite::Row<'_>> for DbCoin {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DbAddress {
pub address: bitcoin::Address,
pub receive_address: bitcoin::Address,
pub change_address: bitcoin::Address,
pub derivation_index: bip32::ChildNumber,
}
@ -203,15 +214,21 @@ impl TryFrom<&rusqlite::Row<'_>> for DbAddress {
type Error = rusqlite::Error;
fn try_from(row: &rusqlite::Row) -> Result<Self, Self::Error> {
let address: String = row.get(0)?;
let address = bitcoin::Address::from_str(&address).expect("We only store valid addresses");
let receive_address: String = row.get(0)?;
let receive_address =
bitcoin::Address::from_str(&receive_address).expect("We only store valid addresses");
let derivation_index: u32 = row.get(1)?;
let change_address: String = row.get(1)?;
let change_address =
bitcoin::Address::from_str(&change_address).expect("We only store valid addresses");
let derivation_index: u32 = row.get(2)?;
let derivation_index = bip32::ChildNumber::from(derivation_index);
assert!(derivation_index.is_normal());
Ok(DbAddress {
address,
receive_address,
change_address,
derivation_index,
})
}

View File

@ -1,8 +1,11 @@
use crate::database::sqlite::{schema::SCHEMA, FreshDbOptions, SqliteDbError, DB_VERSION};
use crate::database::sqlite::{
schema::{DbWallet, SCHEMA},
FreshDbOptions, SqliteDbError, DB_VERSION,
};
use std::{convert::TryInto, fs, path, time};
use miniscript::bitcoin::secp256k1;
use miniscript::bitcoin::{self, secp256k1};
pub const LOOK_AHEAD_LIMIT: u32 = 200;
@ -95,14 +98,19 @@ pub fn create_fresh_db(
// necessarily 0.
let mut query = String::with_capacity(100 * LOOK_AHEAD_LIMIT as usize);
for index in 0..LOOK_AHEAD_LIMIT {
// TODO: have this as a helper in descriptors.rs
let address = options
let receive_address = options
.main_descriptor
.receive_descriptor()
.derive(index.into(), secp)
.address(options.bitcoind_network);
let change_address = options
.main_descriptor
.change_descriptor()
.derive(index.into(), secp)
.address(options.bitcoind_network);
query += &format!(
"INSERT INTO addresses (address, derivation_index) VALUES (\"{}\", {});\n",
address, index
"INSERT INTO addresses (receive_address, change_address, derivation_index) VALUES (\"{}\", \"{}\", {});\n",
receive_address, change_address, index
);
}
@ -118,9 +126,9 @@ pub fn create_fresh_db(
rusqlite::params![options.bitcoind_network.to_string()],
)?;
tx.execute(
"INSERT INTO wallets (timestamp, main_descriptor, deposit_derivation_index) \
VALUES (?1, ?2, ?3)",
rusqlite::params![timestamp, options.main_descriptor.to_string(), 0,],
"INSERT INTO wallets (timestamp, main_descriptor, deposit_derivation_index, change_derivation_index) \
VALUES (?1, ?2, ?3, ?4)",
rusqlite::params![timestamp, options.main_descriptor.to_string(), 0, 0],
)?;
tx.execute_batch(&query)?;
@ -129,3 +137,31 @@ pub fn create_fresh_db(
Ok(())
}
/// Insert the deposit and change addresses for this index in the address->index mapping table
pub fn populate_address_mapping(
db_tx: &rusqlite::Transaction,
db_wallet: &DbWallet,
next_index: u32,
network: bitcoin::Network,
secp: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
) -> rusqlite::Result<()> {
// Update the address to derivation index mapping.
let next_la_index = next_index + LOOK_AHEAD_LIMIT - 1;
let next_receive_address = db_wallet
.main_descriptor
.receive_descriptor()
.derive(next_la_index.into(), secp)
.address(network);
let next_change_address = db_wallet
.main_descriptor
.change_descriptor()
.derive(next_la_index.into(), secp)
.address(network);
db_tx.execute(
"INSERT INTO addresses (receive_address, change_address, derivation_index) VALUES (?1, ?2, ?3)",
rusqlite::params![next_receive_address.to_string(), next_change_address.to_string(), next_la_index],
)?;
Ok(())
}

View File

@ -9,7 +9,8 @@ use miniscript::{
descriptor, hash256,
miniscript::{decode::Terminal, Miniscript},
policy::{Liftable, Semantic as SemanticPolicy},
translate_hash_clone, MiniscriptKey, ScriptContext, ToPublicKey, TranslatePk, Translator,
translate_hash_clone, ForEachKey, MiniscriptKey, ScriptContext, ToPublicKey, TranslatePk,
Translator,
};
use std::{collections::BTreeMap, convert::TryFrom, error, fmt, str, sync};
@ -30,7 +31,11 @@ impl std::fmt::Display for DescCreationError {
match self {
Self::InsaneTimelock(tl) => write!(f, "Timelock value '{}' isn't safe to use", tl),
Self::InvalidKey(key) => {
write!(f, "Invalid key '{}'. Need a wildcard ('ranged') xpub", key)
write!(
f,
"Invalid key '{}'. Need a wildcard ('ranged') xpub with a multipath for (and only for) deriving change addresses. That is, an xpub of the form 'xpub.../<0;1>/*'.",
key
)
}
Self::Miniscript(e) => write!(f, "Miniscript error: '{}'.", e),
Self::IncompatibleDesc => write!(f, "Descriptor is not compatible."),
@ -125,6 +130,10 @@ impl MiniscriptKey for DerivedPublicKey {
fn is_x_only_key(&self) -> bool {
false
}
fn num_der_paths(&self) -> usize {
0
}
}
impl ToPublicKey for DerivedPublicKey {
@ -162,17 +171,37 @@ fn csv_check(csv_value: u32) -> Result<(), DescCreationError> {
.map_err(|_| DescCreationError::InsaneTimelock(csv_value))
}
fn is_unhardened_deriv(key: &descriptor::DescriptorPublicKey) -> bool {
// We require the descriptor key to:
// - Be deriveable (to contain a wildcard)
// - Be multipath (to contain a step in the derivation path with multiple indexes)
// - The multipath step to only contain two indexes, 0 and 1.
fn is_valid_desc_key(key: &descriptor::DescriptorPublicKey) -> bool {
match *key {
descriptor::DescriptorPublicKey::Single(..) => false,
descriptor::DescriptorPublicKey::XPub(ref xpub) => {
descriptor::DescriptorPublicKey::Single(..) | descriptor::DescriptorPublicKey::XPub(..) => {
false
}
descriptor::DescriptorPublicKey::MultiXPub(ref xpub) => {
// Rust-miniscript enforces BIP389 which states that all paths must have the same len.
let len = xpub.derivation_paths.get(0).expect("Cannot be empty").len();
xpub.wildcard == descriptor::Wildcard::Unhardened
&& xpub.derivation_paths.len() == 2
&& xpub.derivation_paths[0][len - 1] == 0.into()
&& xpub.derivation_paths[1][len - 1] == 1.into()
}
}
}
/// An [InheritanceDescriptor] that contains multipath keys for (and only for) the receive keychain
/// and the change keychain.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MultipathDescriptor {
multi_desc: descriptor::Descriptor<descriptor::DescriptorPublicKey>,
receive_desc: InheritanceDescriptor,
change_desc: InheritanceDescriptor,
}
/// A Miniscript descriptor with a main, unencombered, branch (the main owner of the coins)
/// and a timelocked branch (the heir).
/// and a timelocked branch (the heir). All keys in this descriptor are singlepath.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InheritanceDescriptor(descriptor::Descriptor<descriptor::DescriptorPublicKey>);
@ -180,16 +209,16 @@ pub struct InheritanceDescriptor(descriptor::Descriptor<descriptor::DescriptorPu
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DerivedInheritanceDescriptor(descriptor::Descriptor<DerivedPublicKey>);
impl fmt::Display for InheritanceDescriptor {
impl fmt::Display for MultipathDescriptor {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
write!(f, "{}", self.multi_desc)
}
}
impl str::FromStr for InheritanceDescriptor {
impl str::FromStr for MultipathDescriptor {
type Err = DescCreationError;
fn from_str(s: &str) -> Result<InheritanceDescriptor, Self::Err> {
fn from_str(s: &str) -> Result<MultipathDescriptor, Self::Err> {
let wsh_desc = descriptor::Wsh::<descriptor::DescriptorPublicKey>::from_str(s)
.map_err(DescCreationError::Miniscript)?;
let ms = match wsh_desc.as_inner() {
@ -197,7 +226,7 @@ impl str::FromStr for InheritanceDescriptor {
_ => return Err(DescCreationError::IncompatibleDesc),
};
let invalid_key = ms.iter_pk().find_map(|pk| {
if is_unhardened_deriv(&pk) {
if is_valid_desc_key(&pk) {
None
} else {
Some(pk)
@ -252,17 +281,47 @@ impl str::FromStr for InheritanceDescriptor {
.iter()
.find(|s| matches!(s, SemanticPolicy::Key(_)))
.ok_or(DescCreationError::IncompatibleDesc)?;
let multi_desc = descriptor::Descriptor::Wsh(wsh_desc);
Ok(InheritanceDescriptor(descriptor::Descriptor::Wsh(wsh_desc)))
// Compute the receive and change "sub" descriptors right away. According to our pubkey
// check above, there must be only two of those, 0 and 1.
// We use /0/* for receiving and /1/* for change.
// FIXME: don't rely on into_single_descs()'s ordering.
let mut singlepath_descs = multi_desc
.clone()
.into_single_descriptors()
.expect("Can't error, all paths have the same length")
.into_iter();
assert_eq!(singlepath_descs.len(), 2);
let receive_desc = InheritanceDescriptor(singlepath_descs.next().expect("First of 2"));
let change_desc = InheritanceDescriptor(singlepath_descs.next().expect("Second of 2"));
Ok(MultipathDescriptor {
multi_desc,
receive_desc,
change_desc,
})
}
}
impl InheritanceDescriptor {
impl fmt::Display for InheritanceDescriptor {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl PartialEq<descriptor::Descriptor<descriptor::DescriptorPublicKey>> for InheritanceDescriptor {
fn eq(&self, other: &descriptor::Descriptor<descriptor::DescriptorPublicKey>) -> bool {
self.0.eq(other)
}
}
impl MultipathDescriptor {
pub fn new(
owner_key: descriptor::DescriptorPublicKey,
heir_key: descriptor::DescriptorPublicKey,
timelock: u16,
) -> Result<InheritanceDescriptor, DescCreationError> {
) -> Result<MultipathDescriptor, DescCreationError> {
// We require the locktime to:
// - not be disabled
// - be in number of blocks
@ -273,7 +332,7 @@ impl InheritanceDescriptor {
if let Some(key) = vec![&owner_key, &heir_key]
.iter()
.find(|k| !is_unhardened_deriv(k))
.find(|k| !is_valid_desc_key(k))
{
return Err(DescCreationError::InvalidKey((**key).clone()));
}
@ -304,69 +363,54 @@ impl InheritanceDescriptor {
.expect("Well typed");
miniscript::Segwitv0::check_local_validity(&tl_miniscript)
.expect("Miniscript must be sane");
Ok(InheritanceDescriptor(descriptor::Descriptor::Wsh(
let multi_desc = descriptor::Descriptor::Wsh(
descriptor::Wsh::new(tl_miniscript).expect("Must pass sanity checks"),
)))
);
// Compute the receive and change "sub" descriptors right away. According to our pubkey
// check above, there must be only two of those, 0 and 1.
// We use /0/* for receiving and /1/* for change.
// FIXME: don't rely on into_single_descs()'s ordering.
let mut singlepath_descs = multi_desc
.clone()
.into_single_descriptors()
.expect("Can't error, all paths have the same length")
.into_iter();
assert_eq!(singlepath_descs.len(), 2);
let receive_desc = InheritanceDescriptor(singlepath_descs.next().expect("First of 2"));
let change_desc = InheritanceDescriptor(singlepath_descs.next().expect("Second of 2"));
Ok(MultipathDescriptor {
multi_desc,
receive_desc,
change_desc,
})
}
pub fn as_inner(&self) -> &descriptor::Descriptor<descriptor::DescriptorPublicKey> {
&self.0
}
/// Derive this descriptor at a given index.
///
/// # Panics
/// - If the given index is hardened.
pub fn derive(
&self,
index: bip32::ChildNumber,
secp: &secp256k1::Secp256k1<impl secp256k1::Verification>,
) -> DerivedInheritanceDescriptor {
assert!(index.is_normal());
// Unfortunately we can't just use `self.0.at_derivation_index().derived_descriptor()`
// since it would return a raw public key, but we need the origin too.
// TODO: upstream our DerivedPublicKey stuff to rust-miniscript.
//
// So we roll our own translation.
struct Derivator<'a, C: secp256k1::Verification>(u32, &'a secp256k1::Secp256k1<C>);
impl<'a, C: secp256k1::Verification>
Translator<
descriptor::DescriptorPublicKey,
DerivedPublicKey,
descriptor::ConversionError,
> for Derivator<'a, C>
{
fn pk(
&mut self,
pk: &descriptor::DescriptorPublicKey,
) -> Result<DerivedPublicKey, descriptor::ConversionError> {
let definite_key = pk.clone().at_derivation_index(self.0);
let origin = (
definite_key.master_fingerprint(),
definite_key.full_derivation_path(),
);
let key = definite_key.derive_public_key(self.1)?;
Ok(DerivedPublicKey { origin, key })
/// Whether all xpubs contained in this descriptor are for the passed expected network.
pub fn all_xpubs_net_is(&self, expected_net: bitcoin::Network) -> bool {
self.multi_desc.for_each_key(|xpub| {
if let descriptor::DescriptorPublicKey::MultiXPub(xpub) = xpub {
xpub.xkey.network == expected_net
} else {
false
}
translate_hash_clone!(
descriptor::DescriptorPublicKey,
DerivedPublicKey,
descriptor::ConversionError
);
}
})
}
let desc = self
.0
.translate_pk(&mut Derivator(index.into(), secp))
.expect("May only fail on hardened derivation indexes, but we ruled out this case.");
DerivedInheritanceDescriptor(desc)
/// Get the descriptor for receiving addresses.
pub fn receive_descriptor(&self) -> &InheritanceDescriptor {
&self.receive_desc
}
/// Get the descriptor for change addresses.
pub fn change_descriptor(&self) -> &InheritanceDescriptor {
&self.change_desc
}
/// Get the value (in blocks) of the relative timelock for the heir's spending path.
pub fn timelock_value(&self) -> u32 {
let wsh_desc = match &self.0 {
let wsh_desc = match &self.multi_desc {
descriptor::Descriptor::Wsh(desc) => desc,
_ => unreachable!(),
};
@ -403,6 +447,65 @@ impl InheritanceDescriptor {
}
}
impl InheritanceDescriptor {
/// Derive this descriptor at a given index for a receiving address.
///
/// # Panics
/// - If the given index is hardened.
pub fn derive(
&self,
index: bip32::ChildNumber,
secp: &secp256k1::Secp256k1<impl secp256k1::Verification>,
) -> DerivedInheritanceDescriptor {
assert!(index.is_normal());
// Unfortunately we can't just use `self.0.at_derivation_index().derived_descriptor()`
// since it would return a raw public key, but we need the origin too.
// TODO: upstream our DerivedPublicKey stuff to rust-miniscript.
//
// So we roll our own translation.
struct Derivator<'a, C: secp256k1::Verification>(u32, &'a secp256k1::Secp256k1<C>);
impl<'a, C: secp256k1::Verification>
Translator<
descriptor::DescriptorPublicKey,
DerivedPublicKey,
descriptor::ConversionError,
> for Derivator<'a, C>
{
fn pk(
&mut self,
pk: &descriptor::DescriptorPublicKey,
) -> Result<DerivedPublicKey, descriptor::ConversionError> {
let definite_key = pk
.clone()
.at_derivation_index(self.0)
.expect("We disallow multipath keys.");
let origin = (
definite_key.master_fingerprint(),
definite_key
.full_derivation_path()
.expect("We disallow multipath keys."),
);
let key = definite_key.derive_public_key(self.1)?;
Ok(DerivedPublicKey { origin, key })
}
translate_hash_clone!(
descriptor::DescriptorPublicKey,
DerivedPublicKey,
descriptor::ConversionError
);
}
DerivedInheritanceDescriptor(
self.0
.translate_pk(&mut Derivator(index.into(), secp))
.expect(
"May only fail on hardened derivation indexes, but we ruled out this case.",
),
)
}
}
/// Map of a raw public key to the xpub used to derive it and its derivation path
pub type Bip32Deriv = BTreeMap<secp256k1::PublicKey, (bip32::Fingerprint, bip32::DerivationPath)>;
@ -454,41 +557,47 @@ mod tests {
#[test]
fn inheritance_descriptor_creation() {
let owner_key = descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/*").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/*").unwrap();
let owner_key = descriptor::DescriptorPublicKey::from_str("xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*").unwrap();
let timelock = 52560;
assert_eq!(InheritanceDescriptor::new(owner_key, heir_key, timelock).unwrap().to_string(), "wsh(or_d(pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/*),and_v(v:pkh(xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/*),older(52560))))#eeyujkt7");
assert_eq!(MultipathDescriptor::new(owner_key, heir_key, timelock).unwrap().to_string(), "wsh(or_d(pk(xpub6Eze7yAT3Y1wGrnzedCNVYDXUqa9NmHVWck5emBaTbXtURbe1NWZbK9bsz1TiVE7Cz341PMTfYgFw1KdLWdzcM1UMFTcdQfCYhhXZ2HJvTW/<0;1>/*),and_v(v:pkh(xpub688Hn4wScQAAiYJLPg9yH27hUpfZAUnmJejRQBCiwfP5PEDzjWMNW1wChcninxr5gyavFqbbDjdV1aK5USJz8NDVjUy7FRQaaqqXHh5SbXe/<0;1>/*),older(52560))))#8n2ydpkt");
// We prevent footguns with timelocks by requiring a u16. Note how the following wouldn't
// compile:
//InheritanceDescriptor::new(owner_key.clone(), heir_key.clone(), 0x00_01_0f_00).unwrap_err();
//InheritanceDescriptor::new(owner_key.clone(), heir_key.clone(), (1 << 31) + 1).unwrap_err();
//InheritanceDescriptor::new(owner_key, heir_key, (1 << 22) + 1).unwrap_err();
//MultipathDescriptor::new(owner_key.clone(), heir_key.clone(), 0x00_01_0f_00).unwrap_err();
//MultipathDescriptor::new(owner_key.clone(), heir_key.clone(), (1 << 31) + 1).unwrap_err();
//MultipathDescriptor::new(owner_key, heir_key, (1 << 22) + 1).unwrap_err();
let owner_key = descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/*").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/*").unwrap();
let owner_key = descriptor::DescriptorPublicKey::from_str("[aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*").unwrap();
let timelock = 57600;
assert_eq!(InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap().to_string(), "wsh(or_d(pk([aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/*),and_v(v:pkh(xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/*),older(57600))))#8kamh6y8");
assert_eq!(MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap().to_string(), "wsh(or_d(pk([aabb0011/10/4893]xpub661MyMwAqRbcFG59fiikD8UV762quhruT8K8bdjqy6N2o3LG7yohoCdLg1m2HAY1W6rfBrtauHkBhbfA4AQ3iazaJj5wVPhwgaRCHBW2DBg/<0;1>/*),and_v(v:pkh(xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/24/32/<0;1>/*),older(57600))))#l6dlpc2l");
// We can't pass a raw key, an xpub that is not deriveable, or only hardened derivable
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/354").unwrap();
InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/*'").unwrap();
InheritanceDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
// We can't pass a raw key, an xpub that is not deriveable, only hardened derivable,
// without both the change and receive derivation paths, or with more than 2 different
// derivation paths.
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/354").unwrap();
MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/<0;1>/*'").unwrap();
MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
let heir_key = descriptor::DescriptorPublicKey::from_str(
"02e24913be26dbcfdf8e8e94870b28725cdae09b448b6c127767bf0154e3a3c8e5",
)
.unwrap();
InheritanceDescriptor::new(owner_key, heir_key, timelock).unwrap_err();
MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/0/*'").unwrap();
MultipathDescriptor::new(owner_key.clone(), heir_key, timelock).unwrap_err();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub661MyMwAqRbcFfxf71L4Dx4w5TmyNXrBicTEAM7vLzumxangwATWWgdJPb6xH1JHcJH9S3jNZx3fCnkkB1WyqrqGgavj1rehHcbythmruvZ/<0;1;2>/*'").unwrap();
MultipathDescriptor::new(owner_key, heir_key, timelock).unwrap_err();
}
#[test]
fn inheritance_descriptor_derivation() {
let secp = secp256k1::Secp256k1::verification_only();
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))#y5wcna2d").unwrap();
let der_desc = desc.derive(11.into(), &secp);
let desc = MultipathDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(10000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))#5f6qd0d9").unwrap();
let der_desc = desc.receive_descriptor().derive(11.into(), &secp);
assert_eq!(
"bc1qvjzcg25nsxmfccct0txjvljxjwn68htkrw57jqmjhfzvhyd2z4msc74w65",
"bc1q26gtczlz03u6juf5cxppapk4sr4fyz53s3g4zs2cgactcahqv6yqc2t8e6",
der_desc.address(bitcoin::Network::Bitcoin).to_string()
);
@ -501,13 +610,13 @@ mod tests {
#[test]
fn inheritance_descriptor_tl_value() {
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(1),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))").unwrap();
let desc = MultipathDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(1),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
assert_eq!(desc.timelock_value(), 1);
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(42000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))").unwrap();
let desc = MultipathDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(42000),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
assert_eq!(desc.timelock_value(), 42000);
let desc = InheritanceDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/*),older(65535),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/*)))").unwrap();
let desc = MultipathDescriptor::from_str("wsh(andor(pk(tpubDEN9WSToTyy9ZQfaYqSKfmVqmq1VVLNtYfj3Vkqh67et57eJ5sTKZQBkHqSwPUsoSskJeaYnPttHe2VrkCsKA27kUaN9SDc5zhqeLzKa1rr/<0;1>/*),older(65535),pk(tpubD8LYfn6njiA2inCoxwM7EuN3cuLVcaHAwLYeups13dpevd3nHLRdK9NdQksWXrhLQVxcUZRpnp5CkJ1FhE61WRAsHxDNAkvGkoQkAeWDYjV/<0;1>/*)))").unwrap();
assert_eq!(desc.timelock_value(), 0xffff);
}

View File

@ -317,6 +317,7 @@ impl DaemonHandle {
bit.clone(),
db.clone(),
config.bitcoin_config.poll_interval_secs,
config.main_descriptor.clone(),
);
// Finally, set up the API.
@ -383,7 +384,7 @@ mod tests {
use super::*;
use crate::{
config::{BitcoinConfig, BitcoindConfig},
descriptors::InheritanceDescriptor,
descriptors::MultipathDescriptor,
testutils::*,
};
@ -510,11 +511,14 @@ mod tests {
stream.flush().unwrap();
}
// Send them a response to 'listdescriptors' with the main descriptor
fn complete_desc_check(server: &net::TcpListener, desc: &str) {
// Send them a response to 'listdescriptors' with the receive and change descriptors
fn complete_desc_check(server: &net::TcpListener, receive_desc: &str, change_desc: &str) {
let net_resp = [
"HTTP/1.1 200\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"descriptors\":[{\"desc\":\"".as_bytes(),
desc.as_bytes(),
receive_desc.as_bytes(),
"\"},".as_bytes(),
"{\"desc\":\"".as_bytes(),
change_desc.as_bytes(),
"\"}]}}\n".as_bytes(),
]
.concat();
@ -594,8 +598,10 @@ mod tests {
};
// Create a dummy config with this bitcoind
let desc_str = "wsh(andor(pk(xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/*),older(10000),pk(xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/*)))#tk6wzexy";
let desc = InheritanceDescriptor::from_str(desc_str).unwrap();
let desc_str = "wsh(andor(pk(xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*),older(10000),pk(xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*)))#yudtr0k5";
let desc = MultipathDescriptor::from_str(desc_str).unwrap();
let receive_desc = desc.receive_descriptor().clone();
let change_desc = desc.change_descriptor().clone();
let config = Config {
bitcoin_config,
bitcoind_config: Some(bitcoind_config),
@ -620,7 +626,7 @@ mod tests {
complete_version_check(&server);
complete_network_check(&server);
complete_wallet_check(&server, &wo_path);
complete_desc_check(&server, desc_str);
complete_desc_check(&server, &receive_desc.to_string(), &change_desc.to_string());
complete_tip_init(&server);
complete_sync_check(&server);
daemon_thread.join().unwrap();
@ -635,7 +641,7 @@ mod tests {
complete_version_check(&server);
complete_network_check(&server);
complete_wallet_check(&server, &wo_path);
complete_desc_check(&server, desc_str);
complete_desc_check(&server, &receive_desc.to_string(), &change_desc.to_string());
complete_sync_check(&server);
daemon_thread.join().unwrap();

View File

@ -2,7 +2,7 @@ use crate::{
bitcoin::{BitcoinError, BitcoinInterface, BlockChainTip, UTxO},
config::{BitcoinConfig, Config},
database::{Coin, DatabaseConnection, DatabaseInterface, SpendBlock},
DaemonHandle,
descriptors, DaemonHandle,
};
use std::{collections::HashMap, env, fs, io, path, process, str::FromStr, sync, thread, time};
@ -44,7 +44,11 @@ impl BitcoinInterface for DummyBitcoind {
true
}
fn received_coins(&self, _: &BlockChainTip) -> Vec<UTxO> {
fn received_coins(
&self,
_: &BlockChainTip,
_: &[descriptors::InheritanceDescriptor],
) -> Vec<UTxO> {
Vec::new()
}
@ -73,7 +77,8 @@ impl BitcoinInterface for DummyBitcoind {
}
pub struct DummyDb {
curr_index: bip32::ChildNumber,
deposit_index: bip32::ChildNumber,
change_index: bip32::ChildNumber,
curr_tip: Option<BlockChainTip>,
coins: HashMap<bitcoin::OutPoint, Coin>,
spend_txs: HashMap<bitcoin::Txid, Psbt>,
@ -82,7 +87,8 @@ pub struct DummyDb {
impl DummyDb {
pub fn new() -> DummyDb {
DummyDb {
curr_index: 0.into(),
deposit_index: 0.into(),
change_index: 0.into(),
curr_tip: None,
coins: HashMap::new(),
spend_txs: HashMap::new(),
@ -119,13 +125,22 @@ impl DatabaseConnection for DummyDbConn {
self.db.write().unwrap().curr_tip = Some(*tip);
}
fn derivation_index(&mut self) -> bip32::ChildNumber {
self.db.read().unwrap().curr_index
fn receive_index(&mut self) -> bip32::ChildNumber {
self.db.read().unwrap().deposit_index
}
fn increment_derivation_index(&mut self, _: &secp256k1::Secp256k1<secp256k1::VerifyOnly>) {
let next_index = self.db.write().unwrap().curr_index.increment().unwrap();
self.db.write().unwrap().curr_index = next_index;
fn change_index(&mut self) -> bip32::ChildNumber {
self.db.read().unwrap().deposit_index
}
fn increment_receive_index(&mut self, _: &secp256k1::Secp256k1<secp256k1::VerifyOnly>) {
let next_index = self.db.write().unwrap().deposit_index.increment().unwrap();
self.db.write().unwrap().deposit_index = next_index;
}
fn increment_change_index(&mut self, _: &secp256k1::Secp256k1<secp256k1::VerifyOnly>) {
let next_index = self.db.write().unwrap().change_index.increment().unwrap();
self.db.write().unwrap().change_index = next_index;
}
fn coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
@ -183,7 +198,10 @@ impl DatabaseConnection for DummyDbConn {
}
}
fn derivation_index_by_address(&mut self, _: &bitcoin::Address) -> Option<bip32::ChildNumber> {
fn derivation_index_by_address(
&mut self,
_: &bitcoin::Address,
) -> Option<(bip32::ChildNumber, bool)> {
None
}
@ -270,10 +288,10 @@ impl DummyMinisafe {
poll_interval_secs: time::Duration::from_secs(2),
};
let owner_key = descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/*").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/*").unwrap();
let owner_key = descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*").unwrap();
let heir_key = descriptor::DescriptorPublicKey::from_str("xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*").unwrap();
let desc =
crate::descriptors::InheritanceDescriptor::new(owner_key, heir_key, 10_000).unwrap();
crate::descriptors::MultipathDescriptor::new(owner_key, heir_key, 10_000).unwrap();
let config = Config {
bitcoin_config,
bitcoind_config: None,

View File

@ -121,7 +121,7 @@ def minisafed(bitcoind, directory):
owner_hd = BIP32.from_seed(os.urandom(32), network="test")
owner_xpub = owner_hd.get_xpub()
main_desc = Descriptor.from_str(f"wsh(or_d(pk({owner_xpub}/*),and_v(v:pkh(tpubD9vQiBdDxYzU4cVFtApWj4devZrvcfWaPXX1zHdDc7GPfUsDKqGnbhraccfm7BAXgRgUbVQUV2v2o4NitjGEk7hpbuP85kvBrD4ahFDtNBJ/*),older(65000))))")
main_desc = Descriptor.from_str(f"wsh(or_d(pk({owner_xpub}/<0;1>/*),and_v(v:pkh(tpubD9vQiBdDxYzU4cVFtApWj4devZrvcfWaPXX1zHdDc7GPfUsDKqGnbhraccfm7BAXgRgUbVQUV2v2o4NitjGEk7hpbuP85kvBrD4ahFDtNBJ/<0;1>/*),older(65000))))")
minisafed = Minisafed(
datadir,

View File

@ -4,4 +4,4 @@ pytest-timeout==1.3.4
ephemeral_port_reserve==1.1.1
bip32~=3.0
bip380==0.0.3
https://github.com/darosior/python-bip380/archive/f25eb2add9a5d461e382635231a5f971652fc8e1.zip

View File

@ -28,7 +28,7 @@ class Minisafed(TailableProc):
self,
datadir,
owner_hd,
main_desc,
multi_desc,
bitcoind_rpc_port,
bitcoind_cookie_path,
):
@ -37,7 +37,8 @@ class Minisafed(TailableProc):
self.prefix = os.path.split(datadir)[-1]
self.owner_hd = owner_hd
self.main_desc = main_desc
self.multi_desc = multi_desc
self.receive_desc, self.change_desc = multi_desc.singlepath_descriptors()
self.conf_file = os.path.join(datadir, "config.toml")
self.cmd_line = [MINISAFED_PATH, "--conf", f"{self.conf_file}"]
@ -49,7 +50,7 @@ class Minisafed(TailableProc):
f.write("daemon = false\n")
f.write(f"log_level = '{LOG_LEVEL}'\n")
f.write(f'main_descriptor = "{main_desc}"\n')
f.write(f'main_descriptor = "{multi_desc}"\n')
f.write("[bitcoin_config]\n")
f.write('network = "regtest"\n')
@ -71,24 +72,32 @@ class Minisafed(TailableProc):
# Sign each input.
for i, psbt_in in enumerate(psbt.i):
# First, gather the needed information from the PSBT input.
# 'hd_keypaths' is of the form {pubkey: (fingerprint (4 bytes), derivation index (4 bytes))}
# 'hd_keypaths' is of the form {pubkey: (fingerprint (4 bytes), derivation path (n * 4 bytes))}
fing_der = next(iter(psbt_in.map[PSBT_IN_BIP32_DERIVATION].values()))
der_index = int.from_bytes(fing_der[4:], byteorder="little", signed=True)
raw_der_path = fing_der[4:]
der_path = [
int.from_bytes(raw_der_path[i : i + 4], byteorder="little", signed=True)
for i in range(0, len(raw_der_path), 4)
]
script_code = psbt_in.map[PSBT_IN_WITNESS_SCRIPT]
# Now sign the transaction with the key of the "owner" (the participant that
# can sign immediately without a timelock)
sighash = sighash_all_witness(script_code, psbt, i)
privkey = coincurve.PrivateKey(
self.owner_hd.get_privkey_from_path([der_index])
self.owner_hd.get_privkey_from_path(der_path)
)
pubkey = privkey.public_key.format()
assert pubkey in psbt_in.map[PSBT_IN_BIP32_DERIVATION].keys(), (
der_path,
fing_der,
pubkey,
psbt_in.map[PSBT_IN_BIP32_DERIVATION].keys(),
)
sig = privkey.sign(sighash, hasher=None) + b"\x01"
logging.debug(f"Adding signature {sig.hex()} for pubkey {pubkey.hex()}")
logging.debug(
f"Adding signature {sig.hex()} for pubkey {pubkey.hex()} (path {der_path})"
)
assert PSBT_IN_PARTIAL_SIG not in psbt_in.map
psbt_in.map[PSBT_IN_PARTIAL_SIG] = {pubkey: sig}
@ -108,12 +117,19 @@ class Minisafed(TailableProc):
# First, gather the needed information from the PSBT input.
# 'hd_keypaths' is of the form {pubkey: (fingerprint, derivation index)}
fing_der = next(iter(psbt_in.map[PSBT_IN_BIP32_DERIVATION].values()))
der_index = int.from_bytes(fing_der[4:], byteorder="little", signed=True)
raw_der_path = fing_der[4:]
der_path = [
int.from_bytes(raw_der_path[i : i + 4], byteorder="little", signed=True)
for i in range(0, len(raw_der_path), 4)
]
assert len(der_path) == 2
# Create a copy of the descriptor to derive it at the index used in this input.
# Then create a satisfaction for it using the signature we just created.
desc = Descriptor.from_str(str(self.main_desc))
desc.derive(der_index)
desc = Descriptor.from_str(
str(self.receive_desc if der_path[0] == 0 else self.change_desc)
)
desc.derive(der_path[1])
sat_material = SatisfactionMaterial(
signatures=psbt_in.map[PSBT_IN_PARTIAL_SIG],
)

View File

@ -88,6 +88,7 @@ def test_create_spend(minisafed, bitcoind):
bitcoind.generate_block(1, wait_for_mempool=txid)
txid = bitcoind.rpc.sendtoaddress(addr, 0.3556)
bitcoind.generate_block(1, wait_for_mempool=txid)
wait_for(lambda: len(minisafed.rpc.listcoins()["coins"]) == 16)
# Stop the daemon, should be a no-op
minisafed.stop()

55
tests/test_spend.py Normal file
View File

@ -0,0 +1,55 @@
from fixtures import *
from test_framework.serializations import PSBT
from test_framework.utils import wait_for
def test_spend_change(minisafed, bitcoind):
"""We can spend a coin that was received on a change address."""
# Receive a coin on a receive address
addr = minisafed.rpc.getnewaddress()["address"]
txid = bitcoind.rpc.sendtoaddress(addr, 0.01)
bitcoind.generate_block(1, wait_for_mempool=txid)
wait_for(lambda: len(minisafed.rpc.listcoins()["coins"]) == 1)
# Create a transaction that will spend this coin to 1) one of our receive
# addresses 2) an external address 3) one of our change addresses.
outpoints = [c["outpoint"] for c in minisafed.rpc.listcoins()["coins"]]
destinations = {
bitcoind.rpc.getnewaddress(): 100_000,
minisafed.rpc.getnewaddress()["address"]: 100_000,
}
res = minisafed.rpc.createspend(outpoints, destinations, 2)
assert "psbt" in res
# The transaction must contain a change output.
spend_psbt = PSBT.from_base64(res["psbt"])
assert len(spend_psbt.o) == 3
assert len(spend_psbt.tx.vout) == 3
# Sign and broadcast this first Spend transaction.
signed_psbt = minisafed.sign_psbt(spend_psbt)
minisafed.rpc.updatespend(signed_psbt.to_base64())
spend_txid = signed_psbt.tx.txid().hex()
minisafed.rpc.broadcastspend(spend_txid)
bitcoind.generate_block(1, wait_for_mempool=spend_txid)
wait_for(lambda: len(minisafed.rpc.listcoins()["coins"]) == 3)
# Now create a new transaction that spends the change output as well as
# the output sent to the receive address.
outpoints = [
c["outpoint"]
for c in minisafed.rpc.listcoins()["coins"]
if c["spend_info"] is None
]
destinations = {
bitcoind.rpc.getnewaddress(): 100_000,
}
res = minisafed.rpc.createspend(outpoints, destinations, 2)
spend_psbt = PSBT.from_base64(res["psbt"])
# We can sign and broadcast it.
signed_psbt = minisafed.sign_psbt(spend_psbt)
minisafed.rpc.updatespend(signed_psbt.to_base64())
spend_txid = signed_psbt.tx.txid().hex()
minisafed.rpc.broadcastspend(spend_txid)
bitcoind.generate_block(1, wait_for_mempool=spend_txid)