The most notable change is rust-bitcoin's change in the serialization of transaction with no input. It now accounts for the segwit marker even for those. The base tx weight in coin selection had to be adapted to handle this. See https://gnusha.org/bitcoin-rust/2024-01-04.log for details.
493 lines
14 KiB
Rust
493 lines
14 KiB
Rust
use crate::{
|
|
bitcoin::{BitcoinInterface, Block, BlockChainTip, MempoolEntry, SyncProgress, UTxO},
|
|
config::{BitcoinConfig, Config},
|
|
database::{BlockInfo, Coin, CoinStatus, DatabaseConnection, DatabaseInterface, LabelItem},
|
|
descriptors, DaemonHandle,
|
|
};
|
|
|
|
use std::{
|
|
collections::{HashMap, HashSet},
|
|
env, fs, io, path, process,
|
|
str::FromStr,
|
|
sync, thread, time,
|
|
};
|
|
|
|
use miniscript::{
|
|
bitcoin::{self, bip32, psbt::Psbt, secp256k1, Transaction, Txid},
|
|
descriptor,
|
|
};
|
|
|
|
pub struct DummyBitcoind {
|
|
pub txs: HashMap<Txid, (Transaction, Option<Block>)>,
|
|
}
|
|
|
|
impl DummyBitcoind {}
|
|
|
|
impl DummyBitcoind {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
txs: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl BitcoinInterface for DummyBitcoind {
|
|
fn genesis_block(&self) -> BlockChainTip {
|
|
let hash = bitcoin::BlockHash::from_str(
|
|
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
|
|
)
|
|
.unwrap();
|
|
BlockChainTip { hash, height: 0 }
|
|
}
|
|
|
|
fn sync_progress(&self) -> SyncProgress {
|
|
SyncProgress::new(1.0, 1_000, 1_000)
|
|
}
|
|
|
|
fn chain_tip(&self) -> BlockChainTip {
|
|
let hash = bitcoin::BlockHash::from_str(
|
|
"000000007bc154e0fa7ea32218a72fe2c1bb9f86cf8c9ebf9a715ed27fdb229a",
|
|
)
|
|
.unwrap();
|
|
let height = 100;
|
|
BlockChainTip { hash, height }
|
|
}
|
|
|
|
fn is_in_chain(&self, _: &BlockChainTip) -> bool {
|
|
// No reorg
|
|
true
|
|
}
|
|
|
|
fn received_coins(
|
|
&self,
|
|
_: &BlockChainTip,
|
|
_: &[descriptors::SinglePathLianaDesc],
|
|
) -> Vec<UTxO> {
|
|
Vec::new()
|
|
}
|
|
|
|
fn confirmed_coins(
|
|
&self,
|
|
_: &[bitcoin::OutPoint],
|
|
) -> (Vec<(bitcoin::OutPoint, i32, u32)>, Vec<bitcoin::OutPoint>) {
|
|
(Vec::new(), Vec::new())
|
|
}
|
|
|
|
fn spending_coins(&self, _: &[bitcoin::OutPoint]) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
|
|
Vec::new()
|
|
}
|
|
|
|
fn spent_coins(
|
|
&self,
|
|
_: &[(bitcoin::OutPoint, bitcoin::Txid)],
|
|
) -> (
|
|
Vec<(bitcoin::OutPoint, bitcoin::Txid, Block)>,
|
|
Vec<bitcoin::OutPoint>,
|
|
) {
|
|
(Vec::new(), Vec::new())
|
|
}
|
|
|
|
fn common_ancestor(&self, _: &BlockChainTip) -> Option<BlockChainTip> {
|
|
todo!()
|
|
}
|
|
|
|
fn broadcast_tx(&self, _: &bitcoin::Transaction) -> Result<(), String> {
|
|
todo!()
|
|
}
|
|
|
|
fn start_rescan(&self, _: &descriptors::LianaDescriptor, _: u32) -> Result<(), String> {
|
|
todo!()
|
|
}
|
|
|
|
fn rescan_progress(&self) -> Option<f64> {
|
|
None
|
|
}
|
|
|
|
fn block_before_date(&self, _: u32) -> Option<BlockChainTip> {
|
|
todo!()
|
|
}
|
|
|
|
fn tip_time(&self) -> Option<u32> {
|
|
todo!()
|
|
}
|
|
|
|
fn wallet_transaction(
|
|
&self,
|
|
txid: &bitcoin::Txid,
|
|
) -> Option<(bitcoin::Transaction, Option<Block>)> {
|
|
self.txs.get(txid).cloned()
|
|
}
|
|
|
|
fn mempool_spenders(&self, _: &[bitcoin::OutPoint]) -> Vec<MempoolEntry> {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
struct DummyDbState {
|
|
deposit_index: bip32::ChildNumber,
|
|
change_index: bip32::ChildNumber,
|
|
curr_tip: Option<BlockChainTip>,
|
|
coins: HashMap<bitcoin::OutPoint, Coin>,
|
|
spend_txs: HashMap<bitcoin::Txid, (Psbt, Option<u32>)>,
|
|
}
|
|
|
|
pub struct DummyDatabase {
|
|
db: sync::Arc<sync::RwLock<DummyDbState>>,
|
|
}
|
|
|
|
impl DatabaseInterface for DummyDatabase {
|
|
fn connection(&self) -> Box<dyn DatabaseConnection> {
|
|
Box::new(DummyDatabase {
|
|
db: self.db.clone(),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl DummyDatabase {
|
|
pub fn new() -> DummyDatabase {
|
|
DummyDatabase {
|
|
db: sync::Arc::new(sync::RwLock::new(DummyDbState {
|
|
deposit_index: 0.into(),
|
|
change_index: 0.into(),
|
|
curr_tip: None,
|
|
coins: HashMap::new(),
|
|
spend_txs: HashMap::new(),
|
|
})),
|
|
}
|
|
}
|
|
|
|
pub fn insert_coins(&mut self, coins: Vec<Coin>) {
|
|
for coin in coins {
|
|
self.db.write().unwrap().coins.insert(coin.outpoint, coin);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DatabaseConnection for DummyDatabase {
|
|
fn network(&mut self) -> bitcoin::Network {
|
|
bitcoin::Network::Bitcoin
|
|
}
|
|
|
|
fn chain_tip(&mut self) -> Option<BlockChainTip> {
|
|
self.db.read().unwrap().curr_tip
|
|
}
|
|
|
|
fn update_tip(&mut self, tip: &BlockChainTip) {
|
|
self.db.write().unwrap().curr_tip = Some(*tip);
|
|
}
|
|
|
|
fn receive_index(&mut self) -> bip32::ChildNumber {
|
|
self.db.read().unwrap().deposit_index
|
|
}
|
|
|
|
fn set_receive_index(
|
|
&mut self,
|
|
index: bip32::ChildNumber,
|
|
_: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
|
) {
|
|
self.db.write().unwrap().deposit_index = index;
|
|
}
|
|
|
|
fn change_index(&mut self) -> bip32::ChildNumber {
|
|
self.db.read().unwrap().deposit_index
|
|
}
|
|
|
|
fn set_change_index(
|
|
&mut self,
|
|
index: bip32::ChildNumber,
|
|
_: &secp256k1::Secp256k1<secp256k1::VerifyOnly>,
|
|
) {
|
|
self.db.write().unwrap().change_index = index;
|
|
}
|
|
|
|
fn coins(
|
|
&mut self,
|
|
statuses: &[CoinStatus],
|
|
outpoints: &[bitcoin::OutPoint],
|
|
) -> HashMap<bitcoin::OutPoint, Coin> {
|
|
self.db
|
|
.read()
|
|
.unwrap()
|
|
.coins
|
|
.clone()
|
|
.into_iter()
|
|
.filter_map(|(op, c)| {
|
|
if (c.block_info.is_none()
|
|
&& c.spend_txid.is_none()
|
|
&& statuses.contains(&CoinStatus::Unconfirmed))
|
|
|| (c.block_info.is_some()
|
|
&& c.spend_txid.is_none()
|
|
&& statuses.contains(&CoinStatus::Confirmed))
|
|
|| (c.spend_txid.is_some()
|
|
&& c.spend_block.is_none()
|
|
&& statuses.contains(&CoinStatus::Spending))
|
|
|| (c.spend_block.is_some() && statuses.contains(&CoinStatus::Spent))
|
|
|| statuses.is_empty()
|
|
{
|
|
Some((op, c))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.filter_map(|(op, c)| {
|
|
if outpoints.contains(&op) || outpoints.is_empty() {
|
|
Some((op, c))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn list_spending_coins(&mut self) -> HashMap<bitcoin::OutPoint, Coin> {
|
|
let mut result = HashMap::new();
|
|
for (k, v) in self.db.read().unwrap().coins.iter() {
|
|
if v.spend_txid.is_some() {
|
|
result.insert(*k, *v);
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
fn new_unspent_coins<'a>(&mut self, coins: &[Coin]) {
|
|
for coin in coins {
|
|
self.db.write().unwrap().coins.insert(coin.outpoint, *coin);
|
|
}
|
|
}
|
|
|
|
fn remove_coins(&mut self, outpoints: &[bitcoin::OutPoint]) {
|
|
for op in outpoints {
|
|
self.db.write().unwrap().coins.remove(op);
|
|
}
|
|
}
|
|
|
|
fn confirm_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, i32, u32)]) {
|
|
for (op, height, time) in outpoints {
|
|
let mut db = self.db.write().unwrap();
|
|
let coin = &mut db.coins.get_mut(op).unwrap();
|
|
assert!(coin.block_info.is_none());
|
|
coin.block_info = Some(BlockInfo {
|
|
height: *height,
|
|
time: *time,
|
|
});
|
|
}
|
|
}
|
|
|
|
fn spend_coins<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)]) {
|
|
for (op, spend_txid) in outpoints {
|
|
let mut db = self.db.write().unwrap();
|
|
let spent = &mut db.coins.get_mut(op).unwrap();
|
|
assert!(spent.spend_txid.is_none());
|
|
assert!(spent.spend_block.is_none());
|
|
spent.spend_txid = Some(*spend_txid);
|
|
}
|
|
}
|
|
|
|
fn unspend_coins<'a>(&mut self, outpoints: &[bitcoin::OutPoint]) {
|
|
for op in outpoints {
|
|
let mut db = self.db.write().unwrap();
|
|
let spent = &mut db.coins.get_mut(op).unwrap();
|
|
assert!(spent.spend_txid.is_some());
|
|
spent.spend_txid = None;
|
|
spent.spend_block = None;
|
|
}
|
|
}
|
|
|
|
fn confirm_spend<'a>(&mut self, outpoints: &[(bitcoin::OutPoint, bitcoin::Txid, i32, u32)]) {
|
|
for (op, spend_txid, height, time) in outpoints {
|
|
let mut db = self.db.write().unwrap();
|
|
let spent = &mut db.coins.get_mut(op).unwrap();
|
|
assert!(spent.spend_txid.is_some());
|
|
assert!(spent.spend_block.is_none());
|
|
spent.spend_txid = Some(*spend_txid);
|
|
spent.spend_block = Some(BlockInfo {
|
|
height: *height,
|
|
time: *time,
|
|
});
|
|
}
|
|
}
|
|
|
|
fn derivation_index_by_address(
|
|
&mut self,
|
|
_: &bitcoin::Address,
|
|
) -> Option<(bip32::ChildNumber, bool)> {
|
|
None
|
|
}
|
|
|
|
fn coins_by_outpoints(
|
|
&mut self,
|
|
outpoints: &[bitcoin::OutPoint],
|
|
) -> HashMap<bitcoin::OutPoint, Coin> {
|
|
// Very inefficient but hey
|
|
self.db
|
|
.read()
|
|
.unwrap()
|
|
.coins
|
|
.clone()
|
|
.into_iter()
|
|
.filter(|(op, _)| outpoints.contains(op))
|
|
.collect()
|
|
}
|
|
|
|
fn store_spend(&mut self, psbt: &Psbt) {
|
|
let txid = psbt.unsigned_tx.txid();
|
|
self.db
|
|
.write()
|
|
.unwrap()
|
|
.spend_txs
|
|
.insert(txid, (psbt.clone(), None));
|
|
}
|
|
|
|
fn spend_tx(&mut self, txid: &bitcoin::Txid) -> Option<Psbt> {
|
|
self.db
|
|
.read()
|
|
.unwrap()
|
|
.spend_txs
|
|
.get(txid)
|
|
.cloned()
|
|
.map(|x| x.0)
|
|
}
|
|
|
|
fn list_spend(&mut self) -> Vec<(Psbt, Option<u32>)> {
|
|
self.db
|
|
.read()
|
|
.unwrap()
|
|
.spend_txs
|
|
.values()
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
|
|
fn delete_spend(&mut self, txid: &bitcoin::Txid) {
|
|
self.db.write().unwrap().spend_txs.remove(txid);
|
|
}
|
|
|
|
fn rollback_tip(&mut self, _: &BlockChainTip) {
|
|
todo!()
|
|
}
|
|
|
|
fn rescan_timestamp(&mut self) -> Option<u32> {
|
|
None
|
|
}
|
|
|
|
fn set_rescan(&mut self, _: u32) {
|
|
todo!()
|
|
}
|
|
|
|
fn complete_rescan(&mut self) {
|
|
todo!()
|
|
}
|
|
|
|
fn update_labels(&mut self, _items: &HashMap<LabelItem, Option<String>>) {
|
|
todo!()
|
|
}
|
|
|
|
fn labels(&mut self, _items: &HashSet<LabelItem>) -> HashMap<String, String> {
|
|
todo!()
|
|
}
|
|
|
|
fn list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec<bitcoin::Txid> {
|
|
let mut txids_and_time = Vec::new();
|
|
let coins = &self.db.read().unwrap().coins;
|
|
// Get txid and block time of every transactions that happened between start and end
|
|
// timestamps.
|
|
for coin in coins.values() {
|
|
if let Some(time) = coin.block_info.map(|b| b.time) {
|
|
if time >= start && time <= end {
|
|
let row = (coin.outpoint.txid, time);
|
|
if !txids_and_time.contains(&row) {
|
|
txids_and_time.push(row);
|
|
}
|
|
}
|
|
}
|
|
if let Some(time) = coin.spend_block.map(|b| b.time) {
|
|
if time >= start && time <= end {
|
|
let row = (coin.spend_txid.expect("spent_at is not none"), time);
|
|
if !txids_and_time.contains(&row) {
|
|
txids_and_time.push(row);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Apply order and limit
|
|
txids_and_time.sort_by(|(_, t1), (_, t2)| t2.cmp(t1));
|
|
txids_and_time.truncate(limit as usize);
|
|
txids_and_time.into_iter().map(|(txid, _)| txid).collect()
|
|
}
|
|
}
|
|
|
|
pub struct DummyLiana {
|
|
pub tmp_dir: path::PathBuf,
|
|
pub handle: DaemonHandle,
|
|
}
|
|
|
|
static mut COUNTER: sync::atomic::AtomicUsize = sync::atomic::AtomicUsize::new(0);
|
|
fn uid() -> usize {
|
|
unsafe {
|
|
let uid = COUNTER.load(sync::atomic::Ordering::Relaxed);
|
|
COUNTER.fetch_add(1, sync::atomic::Ordering::Relaxed);
|
|
uid
|
|
}
|
|
}
|
|
|
|
pub fn tmp_dir() -> path::PathBuf {
|
|
env::temp_dir().join(format!(
|
|
"lianad-{}-{:?}-{}",
|
|
process::id(),
|
|
thread::current().id(),
|
|
uid(),
|
|
))
|
|
}
|
|
|
|
impl DummyLiana {
|
|
/// Creates a new DummyLiana interface
|
|
pub fn new(
|
|
bitcoin_interface: impl BitcoinInterface + 'static,
|
|
database: impl DatabaseInterface + 'static,
|
|
) -> DummyLiana {
|
|
let tmp_dir = tmp_dir();
|
|
fs::create_dir_all(&tmp_dir).unwrap();
|
|
// Use a shorthand for 'datadir', to avoid overflowing SUN_LEN on MacOS.
|
|
let data_dir: path::PathBuf = [tmp_dir.as_path(), path::Path::new("d")].iter().collect();
|
|
|
|
let network = bitcoin::Network::Bitcoin;
|
|
let bitcoin_config = BitcoinConfig {
|
|
network,
|
|
poll_interval_secs: time::Duration::from_secs(2),
|
|
};
|
|
|
|
let owner_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8KLW4HGLXZBJknja7kDUJuFHnM424LbziEXsfkh1WQCiEjjHw4zLqSUm4rvhgyGkkuRowE9tCJSgt3TQB5J3SKAbZ2SdcKST/<0;1>/*").unwrap());
|
|
let heir_key = descriptors::PathInfo::Single(descriptor::DescriptorPublicKey::from_str("[aabbccdd]xpub68JJTXc1MWK8PEQozKsRatrUHXKFNkD1Cb1BuQU9Xr5moCv87anqGyXLyUd4KpnDyZgo3gz4aN1r3NiaoweFW8UutBsBbgKHzaD5HkTkifK/<0;1>/*").unwrap());
|
|
let policy = descriptors::LianaPolicy::new(
|
|
owner_key,
|
|
[(10_000, heir_key)].iter().cloned().collect(),
|
|
)
|
|
.unwrap();
|
|
let desc = descriptors::LianaDescriptor::new(policy);
|
|
let config = Config {
|
|
bitcoin_config,
|
|
bitcoind_config: None,
|
|
data_dir: Some(data_dir),
|
|
#[cfg(unix)]
|
|
daemon: false,
|
|
log_level: log::LevelFilter::Debug,
|
|
main_descriptor: desc,
|
|
};
|
|
|
|
let handle = DaemonHandle::start(config, Some(bitcoin_interface), Some(database)).unwrap();
|
|
DummyLiana { tmp_dir, handle }
|
|
}
|
|
|
|
#[cfg(feature = "daemon")]
|
|
pub fn rpc_server(self) -> Result<(), io::Error> {
|
|
self.handle.rpc_server()?;
|
|
fs::remove_dir_all(&self.tmp_dir)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn shutdown(self) {
|
|
self.handle.shutdown();
|
|
fs::remove_dir_all(&self.tmp_dir).unwrap();
|
|
}
|
|
}
|