43 Commits

Author SHA1 Message Date
Dave Hrycyszyn
e897916ce8 Fixes to miniscript descriptor 2024-10-19 16:27:33 +01:00
Dave Hrycyszyn
01819d3c78 Changing cli command for consistency 2024-07-30 11:01:39 +01:00
Dave Hrycyszyn
8a4c43a0d0 Refactoring 2024-07-29 11:53:17 +01:00
Dave Hrycyszyn
aacec96569 Minor re-ordering 2024-07-29 11:20:29 +01:00
Dave Hrycyszyn
cac76373ec Docs 2024-07-29 11:10:50 +01:00
Dave Hrycyszyn
f607bf25d3 Removed public accessor on htlc policy creation 2024-07-29 11:10:39 +01:00
Dave Hrycyszyn
cea9d8ead5 Renamed htlc to create_htlc so the driver is distinguished from the htlc model 2024-07-29 11:08:29 +01:00
Dave Hrycyszyn
f78784973c Closed off unnecessary public fn access in key generation 2024-07-29 11:06:56 +01:00
Dave Hrycyszyn
42dbf738c6 ibid 2024-07-29 11:02:57 +01:00
Dave Hrycyszyn
474bc84252 ibid 2024-07-29 11:02:41 +01:00
Dave Hrycyszyn
62305c4213 Renaming a few things in HTLC creation to make flow more memorable 2024-07-29 11:02:26 +01:00
Dave Hrycyszyn
170723d05e Printing sammy's balance at the end of HTLC creation 2024-07-29 11:01:58 +01:00
Dave Hrycyszyn
bcd76c2ebc Sanity checking htlc output and logging from creation point 2024-07-29 11:01:36 +01:00
Dave Hrycyszyn
a7a66ad43b Ditching unnecesary wallet sync logger statements 2024-07-29 11:01:03 +01:00
Dave Hrycyszyn
03ecff9541 Printing wwallet sync to log 2024-07-29 11:00:45 +01:00
Dave Hrycyszyn
ebe5283ed2 Renaming external_public_key to public_key 2024-07-29 11:00:31 +01:00
Dave Hrycyszyn
192d0c3da2 Removing code form incomplete policy transfer 2024-07-29 10:41:48 +01:00
Dave Hrycyszyn
82e8612b38 Docs on transfer operations 2024-07-29 10:41:28 +01:00
Dave Hrycyszyn
6b55b0d0a4 Fixing typo 2024-07-29 10:41:15 +01:00
Dave Hrycyszyn
2ced899c6b Using the sha256 utils to generate a hash preimage for the htlc 2024-07-29 10:41:10 +01:00
Dave Hrycyszyn
e1e2f49957 Using the keys::load_from_file function 2024-07-29 10:11:15 +01:00
Dave Hrycyszyn
e4eedbd206 HTLC broadcast working 2024-07-29 10:03:02 +01:00
Dave Hrycyszyn
918544a76b Using Signet network 2024-07-29 09:54:57 +01:00
Dave Hrycyszyn
1fb1bea9aa Removed unused imports 2024-07-26 15:46:07 +01:00
Dave Hrycyszyn
221009829e Start of a new transfer policy 2024-07-26 15:45:58 +01:00
Dave Hrycyszyn
2f4d1d7ee8 Ditching wallet sync on setup, drivers can do it themselves 2024-07-26 15:45:34 +01:00
Dave Hrycyszyn
8db2ca6861 Set up default tracing level of info and above 2024-07-26 15:45:12 +01:00
Dave Hrycyszyn
dcf4761940 Refactored bitcoin driver programs 2024-07-26 15:27:32 +01:00
Dave Hrycyszyn
0edfbfaab1 Simplifying Bitcoin client creation a bit 2024-07-25 20:16:56 +01:00
Dave Hrycyszyn
b031fbc244 We only have one bitcoin client now, simplifying 2024-07-25 20:15:24 +01:00
Dave Hrycyszyn
15e217a6d4 Removed unused dependencies 2024-07-25 20:12:16 +01:00
Dave Hrycyszyn
aa812d4101 Renamed EsploraWallet to BitcoinClient 2024-07-25 20:11:08 +01:00
Dave Hrycyszyn
4b63245bfe Major refactor of the Esplora client
Now we're back to using regular bdk types, which is a big advantage.
2024-07-25 19:54:37 +01:00
Dave Hrycyszyn
e4e8298fcd Adding an htlc model file 2024-07-25 19:12:39 +01:00
Dave Hrycyszyn
62a8a7020c Experimenting with two-party PSBT signing. 2024-07-25 12:34:26 +01:00
Dave Hrycyszyn
09c10b8d45 Simplified driver code a bit 2024-07-24 18:14:08 +01:00
Dave Hrycyszyn
479abcbba2 Using tracing and simplifying a lot of console output in Bitcoin driver 2024-07-24 17:33:40 +01:00
Dave Hrycyszyn
b78aadabff Moved the Electrum client back down 2024-07-24 16:52:35 +01:00
Dave Hrycyszyn
e2c963983c Ditched the unused run() function in the Bitcoin driver 2024-07-24 16:51:23 +01:00
Dave Hrycyszyn
c65bc369ce HTLC cli driver working (no implementation) 2024-07-24 16:46:02 +01:00
Dave Hrycyszyn
3d19cc50fb Starting work on HTLC commands 2024-07-24 16:39:19 +01:00
Dave Hrycyszyn
931cba9cde Disabled electrum client 2024-07-24 16:24:49 +01:00
Dave Hrycyszyn
c5d52dd537 Refactored wallet creation so it's outside of either esplora or electrum client code 2024-07-24 16:18:41 +01:00
21 changed files with 467 additions and 1445 deletions

1144
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -72,21 +72,28 @@ Later, we will aim to remove the Side Watcher from the architecture, by (a) movi
## Bitcoin integration
There is a Bitcoin client integrated into the node, which can do simple coin transfers using esplora and the Mutinynet server's Signet (30 second blocktime).
There is an Esplora Bitcoin client integrated into the node
### Simple coin transfers
The client can do simple coin transfers using esplora and the Mutinynet server's Signet (30 second blocktime).
The client's demo driver can be run by doing:
```
cargo run -- init dave
cargo run -- init sammy
cargo run -- btc
cargo run -- btc-transfer
```
You'll need to have funded the "dave" address prior to running the `btc` command - otherwise the transfer will fail gracefully.
I was using this primarily as a way to experiment with constructing and broadcasting Bitcoin transactions, with the hope that it would be possible to move on to more advanced constructions (e.g. state channels). However, now that I look at all the options, it seems that multi-party state channels in Bitcoin are (probably) impossible to construct.
I have been using this primarily as a way to experiment with constructing and broadcasting Bitcoin transactions, with the hope that it would be possible to move on to more advanced constructions (e.g. state channels).
### HTLCs (in progress)
An experimental driver for Bitcoin Hash Time Locked Contracts (HTLCs).
There is a second, unused Bitcoin client in place which uses Blockstream's Electrum server, but this didn't seem to be working properly with respect to Signet Bitcoin network during my testing, so I went with the esplora / Mutiny version instead.
## Possible uses

View File

@@ -8,28 +8,26 @@ edition = "2021"
[dependencies]
anyhow = "1.0.86"
async-trait = "0.1.52"
bdk = { version = "0.29.0", default-feature = false, features = ["all-keys"] }
bdk_esplora = "0.15.0"
bdk_sqlite = "0.2.0"
bdk_wallet = { version = "1.0.0-alpha.13", features = ["all-keys"] }
bdk = { version = "0.29.0", features = [
"compiler",
"use-esplora-blocking",
"std",
"keys-bip39",
], default-features = false }
bft-json-crdt = { path = "../crates/bft-json-crdt" }
bft-crdt-derive = { path = "../crates/bft-json-crdt/bft-crdt-derive" }
bitcoin = { version = "0.32.2", features = ["rand"] }
clap = { version = "4.5.4", features = ["derive"] }
dirs = "5.0.1"
electrum-client = "0.20"
ezsockets = { version = "*", features = ["client"] }
fastcrypto = "0.1.8"
indexmap = { version = "2.2.6", features = ["serde"] }
reqwest = { version = "*", features = ["blocking"] }
# serde_cbor = "0.11.2" # move to this once we need to pack things in CBOR
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.117"
sha256 = "1.5.0"
tokio = { version = "1.37.0", features = ["full"] }
toml = "0.8.14"
tracing = "0.1.32"
# tracing-subscriber = "0.3.9"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] }
[dev-dependencies]

View File

@@ -39,7 +39,7 @@ impl ezsockets::ClientExt for Client {
/// When we receive a text message, apply the bft-crdt operation contained in it to our
/// local bft-crdt.
async fn on_text(&mut self, text: String) -> Result<(), ezsockets::Error> {
let string_sha = utils::shassy(text.clone());
let string_sha = utils::sha256(text.clone());
println!("received text, sha: {string_sha}");
let incoming: bft_json_crdt::json_crdt::SignedOp = serde_json::from_str(&text).unwrap();
let object_sha = utils::shappy(incoming.clone());

View File

@@ -0,0 +1,115 @@
use bdk::{
bitcoin::{psbt::PartiallySignedTransaction, secp256k1::PublicKey, Network, Transaction},
blockchain::EsploraBlockchain,
database::MemoryDatabase,
keys::ExtendedKey,
template::Bip84,
wallet::AddressInfo,
KeychainKind, SignOptions, SyncOptions, Wallet,
};
use crate::{bitcoin::keys, utils};
/// A client that uses Esplora to interact with the Bitcoin network.
pub struct BitcoinClient {
pub(crate) blockchain: bdk::blockchain::EsploraBlockchain,
name: String,
pub(crate) wallet: Wallet<MemoryDatabase>,
pub(crate) public_key: PublicKey,
}
impl BitcoinClient {
pub(crate) fn sync(&self) -> anyhow::Result<()> {
tracing::info!("syncing {}'s wallet", self.name);
self.wallet.sync(&self.blockchain, SyncOptions::default())?;
Ok(())
}
pub(crate) fn broadcast(&self, tx: &Transaction) -> anyhow::Result<()> {
tracing::info!(
"broadcasting transaction, output will be at https://mutinynet.com/tx/{}",
tx.txid()
);
let _ = self.blockchain.broadcast(&tx);
Ok(())
}
/// Builds and signs a send transaction to send coins between addresses.
///
/// Does NOT send it, you must call `broadcast` to do that.
///
/// We could split the creation and signing easily if needed.
pub(crate) fn build_and_sign_send_tx(
&mut self,
recipient: AddressInfo,
amount: u64,
) -> Result<bdk::bitcoin::Transaction, anyhow::Error> {
let mut tx_builder = self.wallet.build_tx();
tx_builder
.add_recipient(recipient.script_pubkey(), amount)
.enable_rbf();
let (mut psbt, _) = tx_builder.finish()?;
let tx = self.sign(&mut psbt, true)?.extract_tx();
Ok(tx)
}
pub(crate) fn sign(
&self,
psbt: &mut PartiallySignedTransaction,
finalize: bool,
) -> Result<PartiallySignedTransaction, anyhow::Error> {
tracing::info!("{} signing PSBT", self.name);
let options = SignOptions {
try_finalize: finalize,
..Default::default()
};
let finalized = self.wallet.sign(psbt, options)?;
// make sure the PSBT is finalized if we asked for it
if finalize {
assert!(finalized)
}
Ok(psbt.to_owned())
}
/// Creates a Bitcoin descriptor wallet with the mnemonic in the given user directory.
pub(crate) fn create(name: &str, network: Network) -> anyhow::Result<BitcoinClient> {
let keys_dir = utils::home(name);
// let mnemonic_path = crate::utils::side_paths(keys_dir).1; // TODO: this tuple stinks
// let words = fs::read_to_string(mnemonic_path).expect("couldn't read bitcoin key file");
let xkey: ExtendedKey = keys::load_from_file(&keys_dir)?;
let xprv = xkey
.into_xprv(Network::Signet)
.expect("couldn't turn xkey into xprv");
let secp = bdk::bitcoin::secp256k1::Secp256k1::new();
let external_descriptor1 = Bip84(xprv.clone(), KeychainKind::External);
let external_descriptor2 = Bip84(xprv, KeychainKind::External);
let internal_descriptor = Some(Bip84(xprv, KeychainKind::Internal));
let wallet = Wallet::new(
external_descriptor1,
internal_descriptor,
network,
MemoryDatabase::default(),
)?;
let blockchain = EsploraBlockchain::new("https://mutinynet.com/api", 20);
let external_public_key = external_descriptor2.0.private_key.public_key(&secp);
let esplora = BitcoinClient {
name: name.to_string(),
wallet,
blockchain,
public_key: external_public_key,
};
Ok(esplora)
}
}

View File

@@ -1,101 +0,0 @@
use crate::{bitcoin, utils};
use bdk::bitcoin::psbt::PartiallySignedTransaction;
use bdk::bitcoin::Network;
use bdk::database::MemoryDatabase;
use bdk::keys::ExtendedKey;
use bdk::template::Bip84;
use bdk::wallet::AddressIndex::{self, New};
use bdk::wallet::AddressInfo;
use bdk::{blockchain::ElectrumBlockchain, electrum_client, SyncOptions};
use bdk::{FeeRate, KeychainKind, SignOptions, TransactionDetails, Wallet};
/// DEPRECATED
///
/// This is a bdk example that uses the Electrum client to interact with the Bitcoin network.
/// Electrum is a light client that connects to a server to get information about the Bitcoin network.
/// The BDK itself does not have the ability to connect to e.g. esplora servers. As the Blockstream Electrum Signet
/// server does not appear to be picking up transactions properly at the moment, I've shifted over to using
/// the (more complex) `bdk_wallet` crate and the esplora client there (see the other bitcoin client).
///
/// Note:the types below are all completely different than the types in `bdk_wallet`.
pub async fn run() -> Result<(), anyhow::Error> {
let dave = utils::home(&"dave".to_string());
let sammy = utils::home(&"sammy".to_string());
let dave_key = bitcoin::keys::load_from_file(&dave).unwrap();
let sammy_key = bitcoin::keys::load_from_file(&sammy).unwrap();
let dave_wallet = create_wallet(dave_key)?;
let sammy_wallet = create_wallet(sammy_key)?;
let dave_address = dave_wallet.get_address(AddressIndex::Peek(0))?.to_string();
let sammy_address = sammy_wallet.get_address(AddressIndex::Peek(0))?.to_string();
println!("Dave's address: {}", dave_address);
println!("Sammy's address: {}", sammy_address);
let blockchain = ElectrumBlockchain::from(electrum_client::Client::new(
"ssl://electrum.blockstream.info:60002",
)?);
println!("Syncing...");
dave_wallet.sync(&blockchain, SyncOptions::default())?;
display_balance(&dave_wallet);
display_balance(&sammy_wallet);
let (mut psbt, details) =
build_sending_tx(&dave_wallet, sammy_wallet.get_address(New)?).expect("psbt build error");
println!("About to sign the transaction: {:?}", details);
dave_wallet.sign(&mut psbt, SignOptions::default())?;
let _signed_tx = psbt.extract_tx();
// println!("Broadcasting...");
// blockchain.broadcast(&signed_tx).expect("broadcast error");
// println!("Transaction ID: {:?}", signed_tx.txid());
Ok(())
}
/// Create a BDK wallet using BIP 84 descriptor ("m/84h/1h/0h/0" and "m/84h/1h/0h/1")
pub fn create_wallet(xkey: ExtendedKey) -> anyhow::Result<Wallet<MemoryDatabase>> {
let xprv = xkey
.into_xprv(Network::Testnet)
.expect("couldn't turn xkey into xprv");
let external_descriptor = Bip84(xprv, KeychainKind::External);
let internal_descriptor = Some(Bip84(xprv, KeychainKind::Internal));
let wallet = Wallet::new(
external_descriptor,
internal_descriptor,
Network::Testnet,
MemoryDatabase::default(),
)?;
Ok(wallet)
}
fn display_balance(wallet: &Wallet<MemoryDatabase>) {
println!(
"Wallet balance for {} after syncing: {:?} sats on network {}",
wallet
.get_address(bdk::wallet::AddressIndex::Peek(0))
.expect("couldn't get address"),
wallet.get_balance().expect("couldn't show balance"),
wallet.network(),
);
}
fn build_sending_tx(
wallet: &Wallet<MemoryDatabase>,
recipient: AddressInfo,
) -> anyhow::Result<(PartiallySignedTransaction, TransactionDetails), anyhow::Error> {
let mut builder = wallet.build_tx();
builder
.add_recipient(recipient.script_pubkey(), 1000)
.enable_rbf()
.do_not_spend_change()
.fee_rate(FeeRate::from_sat_per_vb(7.0));
Ok(builder.finish()?)
}

View File

@@ -1,188 +0,0 @@
use std::{collections::BTreeSet, fs, io::Write};
use bdk::keys::bip39::Mnemonic;
use bdk_esplora::{
esplora_client::{self, AsyncClient},
EsploraAsyncExt,
};
use bdk_wallet::{
bitcoin::{Address, Amount, Network, Script},
chain::ConfirmationTimeHeightAnchor,
keys::{DerivableKey, ExtendedKey},
wallet::AddressInfo,
KeychainKind, SignOptions, Wallet,
};
use bdk_sqlite::{rusqlite::Connection, Store};
use crate::utils;
const STOP_GAP: usize = 50;
const PARALLEL_REQUESTS: usize = 5;
/// A wallet that uses the Esplora client to interact with the Bitcoin network.
pub struct EsploraWallet {
client: AsyncClient,
db: Store<KeychainKind, ConfirmationTimeHeightAnchor>,
name: String,
wallet: Wallet,
}
impl EsploraWallet {
/// Builds and signs a send transaction to send coins between addresses.
///
/// Does NOT send it, you must call `broadcast` to do that.
///
/// We could split the creation and signing easily if needed.
pub(crate) fn build_and_sign_send_tx(
&mut self,
recipient: Address,
amount: Amount,
) -> Result<bitcoin::Transaction, anyhow::Error> {
let mut tx_builder = self.wallet.build_tx();
tx_builder
.add_recipient(recipient.script_pubkey(), amount)
.enable_rbf();
let mut psbt = tx_builder.finish()?;
let finalized = self.wallet.sign(&mut psbt, SignOptions::default())?;
assert!(finalized);
let tx = psbt.extract_tx()?;
Ok(tx)
}
/// Syncs the wallet with the latest state of the Bitcoin blockchain
pub(crate) async fn sync(&mut self) -> Result<(), anyhow::Error> {
print!("Syncing...");
fn generate_inspect(
kind: KeychainKind,
) -> impl FnMut(u32, &Script) + Send + Sync + 'static {
let mut once = Some(());
let mut stdout = std::io::stdout();
move |spk_i, _| {
match once.take() {
Some(_) => print!("\nScanning keychain [{:?}]", kind),
None => print!(" {:<3}", spk_i),
};
stdout.flush().expect("must flush");
}
}
let request = self
.wallet
.start_full_scan()
.inspect_spks_for_all_keychains({
let mut once = BTreeSet::<KeychainKind>::new();
move |keychain, spk_i, _| {
match once.insert(keychain) {
true => print!("\nScanning keychain [{:?}]", keychain),
false => print!(" {:<3}", spk_i),
}
std::io::stdout().flush().expect("must flush")
}
})
.inspect_spks_for_keychain(
KeychainKind::External,
generate_inspect(KeychainKind::External),
)
.inspect_spks_for_keychain(
KeychainKind::Internal,
generate_inspect(KeychainKind::Internal),
);
let mut update = self
.client
.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)
.await?;
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
let _ = update.graph_update.update_last_seen_unconfirmed(now);
self.wallet.apply_update(update)?;
self.persist_local()?;
println!("Sync complete for {}", self.name);
Ok(())
}
fn persist_local(&mut self) -> Result<(), anyhow::Error> {
Ok(if let Some(changeset) = self.wallet.take_staged() {
self.db.write(&changeset)?;
})
}
/// Gets the next unused address from the wallet.
pub(crate) fn next_unused_address(&mut self) -> Result<AddressInfo, anyhow::Error> {
let address = self.wallet.next_unused_address(KeychainKind::External);
self.persist_local()?;
println!(
"Generated address: https://mutinynet.com/address/{}",
address
);
Ok(address)
}
/// Returns the balance of the wallet.
pub(crate) fn balance(&self) -> bdk_wallet::wallet::Balance {
self.wallet.balance()
}
/// Broadcasts a signed transaction to the network.
pub(crate) async fn broadcast(
&self,
tx: &bitcoin::Transaction,
) -> Result<(), esplora_client::Error> {
println!(
"{} broadcasting tx https://mutinynet.com/tx/{}",
self.name,
tx.compute_txid()
);
self.client.broadcast(tx).await
}
}
/// Creates a Bitcoin descriptor wallet with the mnemonic in the given user directory.
pub(crate) fn create_wallet(name: &str, network: Network) -> anyhow::Result<EsploraWallet> {
let keys_dir = utils::home(name);
let mnemonic_path = crate::utils::side_paths(keys_dir).1; // TODO: this tuple stinks
let mnemonic_words = fs::read_to_string(mnemonic_path).expect("couldn't read bitcoin key file");
println!("Creating wallet from mnemonic: {mnemonic_words}");
let mnemonic = Mnemonic::parse(mnemonic_words).unwrap();
// Generate the extended key
let xkey: ExtendedKey = mnemonic
.into_extended_key()
.expect("couldn't turn mnemonic into xkey");
let xprv = xkey
.into_xprv(Network::Signet)
.expect("problem converting xkey to xprv")
.to_string();
println!("Setting up esplora database for {name}");
let db_path = format!("/tmp/{name}-bdk-esplora-async-example.sqlite");
let conn = Connection::open(db_path)?;
let mut db = Store::new(conn)?;
let external_descriptor = format!("wpkh({xprv}/84'/1'/0'/0/*)");
let internal_descriptor = format!("wpkh({xprv}/84'/1'/0'/1/*)");
let changeset = db.read().expect("couldn't read esplora database");
let wallet = Wallet::new_or_load(
&external_descriptor,
&internal_descriptor,
changeset,
network,
)
.expect("problem setting up wallet");
let client = esplora_client::Builder::new("https://mutinynet.com/api")
.build_async()
.expect("couldn't build esplora client");
let esplora = EsploraWallet {
name: name.to_string(),
wallet,
db,
client,
};
Ok(esplora)
}

View File

@@ -1,2 +0,0 @@
pub mod electrum;
pub mod esplora;

View File

@@ -1,60 +0,0 @@
use bdk_wallet::bitcoin::{Amount, Network};
use crate::bitcoin::clients;
/// Demonstrates the use of bdk with the Esplora client.
///
/// This is more complex than the bare `bdk` crate, but the esplora client works.
///
/// Also, it very handily works with the mutinynet.com esplora server, which is configured
/// with 30 second block times.
pub(crate) async fn run() -> Result<(), anyhow::Error> {
simple_transfer().await
}
async fn simple_transfer() -> Result<(), anyhow::Error> {
let mut dave = clients::esplora::create_wallet("dave", Network::Signet)?;
let mut sammy = clients::esplora::create_wallet("sammy", Network::Signet)?;
let _next_address = dave.next_unused_address()?;
let dave_balance = dave.balance();
println!(
"Dave wallet balance before syncing: {} sats",
dave_balance.total()
);
dave.sync().await?;
let dave_balance = dave.balance();
println!("Wallet balance after syncing: {} sats", dave_balance);
let sammy_address = sammy.next_unused_address()?.address;
println!("Sammy's address: {}", sammy_address);
let sammy_balance = sammy.balance();
println!(
"Sammy wallet balance before syncing: {} sats",
sammy_balance
);
sammy.sync().await?;
let sammy_balance = sammy.balance();
println!("Sammy wallet balance after syncing: {} sats", sammy_balance);
let send_amount = Amount::from_sat(500);
if dave_balance.total() < send_amount {
println!(
"Please send at least {} sats to the receiving address",
send_amount
);
std::process::exit(0);
}
let tx = dave.build_and_sign_send_tx(sammy_address, send_amount)?;
dave.broadcast(&tx).await?;
Ok(())
}

View File

@@ -0,0 +1,51 @@
use crate::bitcoin::{self, driver};
use crate::utils;
use bdk::wallet::AddressIndex::New;
use bdk::SignOptions;
pub(crate) async fn run() -> anyhow::Result<()> {
tracing::info!("starting htlc flow");
let (dave, sammy) = driver::setup().await?;
let _ = dave.sync();
let _ = sammy.sync();
// Create an HTLC descriptor with a redeem identity, a hashlock from the preimage,
// a refund timelock, and a refund identity
let value = 500;
let recipient = sammy.wallet.get_address(New)?.script_pubkey();
let hash_preimage = "blah".to_string();
let hashlock = utils::sha256(hash_preimage);
let htlc = bitcoin::htlc::Htlc::new(dave.public_key, hashlock, 100, sammy.public_key);
let htlc_descriptor = htlc.to_miniscript_descriptor();
// format a new commitment transaction like in Lightning
let mut commitment_builder = dave.wallet.build_tx();
commitment_builder.enable_rbf();
let (psbt, _) = commitment_builder
.finish()
.expect("unable to build commitment");
// sign the commitment transaction
let mut dave_psbt = dave.sign(&mut psbt.clone(), false)?;
let sammy_psbt = sammy.sign(&mut psbt.clone(), false)?;
dave_psbt
.combine(sammy_psbt)
.expect("problem combining bitcoin PSBTs"); // these guys love mutability
let finalized = dave
.wallet
.finalize_psbt(&mut dave_psbt, SignOptions::default())
.expect("couldn't finalize");
assert!(finalized);
let tx = dave_psbt.extract_tx();
let _ = dave.broadcast(&tx)?;
let _ = sammy.sync();
let sammy_balance = sammy.wallet.get_balance()?;
tracing::info!("sammy balance: {}", sammy_balance);
Ok(())
}

View File

@@ -0,0 +1,27 @@
use super::client::BitcoinClient;
use bdk::bitcoin::Network;
use tracing::Level;
use tracing_subscriber::{filter, fmt, layer::Layer, prelude::*, Registry};
pub mod create_htlc;
pub mod policy_transfer;
pub mod simple_transfer;
async fn setup() -> Result<(BitcoinClient, BitcoinClient), anyhow::Error> {
tracing_setup();
let dave = BitcoinClient::create("dave", Network::Signet)?;
let sammy = BitcoinClient::create("sammy", Network::Signet)?;
Ok((dave, sammy))
}
fn tracing_setup() {
// show only info level logs and above:
let info = filter::LevelFilter::from_level(Level::INFO);
// set up the tracing subscriber:
let subscriber = Registry::default().with(fmt::layer().with_filter(info));
tracing::subscriber::set_global_default(subscriber).unwrap();
tracing::info!("Tracing initialized.");
}

View File

@@ -0,0 +1,23 @@
use std::str::FromStr;
use bdk::miniscript::policy;
use crate::bitcoin;
/// A miniscript-based simple transfer, equivalent to the `simple_transfer`
/// but using a Bitcoin miniscript policy. TODO: finish implementation, it's not
/// working yet.
pub(crate) async fn run() -> anyhow::Result<()> {
let (dave, _sammy) = bitcoin::driver::setup().await?;
tracing::info!("starting transfer policy flow");
let policy_str = format!("addr(bc1qgw6xanldsz959z45y4dszehx4xkuzf7nfhya8x)");
let policy = policy::Concrete::<bdk::bitcoin::PublicKey>::from_str(&policy_str)
.expect("policy compilation failed")
.to_owned();
tracing::info!("policy: {}", policy);
Ok(())
}

View File

@@ -0,0 +1,30 @@
use crate::bitcoin::client::BitcoinClient;
use crate::bitcoin::driver;
use bdk::wallet::AddressIndex;
/// Run the simplest transfer flow. There is no policy file,
/// it's just a normal bitcoin transaction for a sanity check.
pub async fn run() -> Result<(), anyhow::Error> {
let (mut dave, sammy) = driver::setup().await?;
let send_amount = 500;
let _ = ensure_enough_sats(&dave, send_amount);
let sammy_address = sammy.wallet.get_address(AddressIndex::New)?;
let tx = dave.build_and_sign_send_tx(sammy_address, send_amount)?;
dave.broadcast(&tx)?;
Ok(())
}
/// Exit if the wallet does not have enough sats to send.
fn ensure_enough_sats(wallet: &BitcoinClient, send_amount: u64) -> anyhow::Result<()> {
if wallet.wallet.get_balance()?.get_total() < send_amount {
tracing::error!(
"Please send at least {} sats to the receiving address. Exiting.",
send_amount
);
std::process::exit(0);
}
Ok(())
}

View File

@@ -0,0 +1,58 @@
use std::str::FromStr;
use bdk::{
bitcoin::secp256k1::PublicKey,
miniscript::{descriptor::Wsh, policy::Concrete},
};
/// A hash time locked contract between two parties.
///
/// If the hash preimage of the hashlock is revealed, the value is sent to the redeem_identity.
///
/// Alternately, if the refund timelock expires, the value can be refunded to the refund_identity.
pub(crate) struct Htlc {
redeem_identity: PublicKey,
hashlock: String,
refund_timelock: u64,
refund_indentiy: PublicKey,
}
impl Htlc {
/// Create a new HTLC.
pub(crate) fn new(
redeem_identity: PublicKey,
hashlock: String,
refund_timelock: u64,
refund_indentiy: PublicKey,
) -> Self {
Self {
redeem_identity,
hashlock,
refund_timelock,
refund_indentiy,
}
}
pub(crate) fn to_miniscript_descriptor(&self) -> Wsh<String> {
let htlc_descriptor = Wsh::new(
self.to_miniscript_policy()
.compile()
.expect("Policy compilation only fails on resource limits or mixed timelocks"),
)
.expect("Resource limits");
assert!(htlc_descriptor.sanity_check().is_ok());
tracing::info!("descriptor: {}", htlc_descriptor);
htlc_descriptor
}
fn to_miniscript_policy(&self) -> Concrete<String> {
Concrete::<String>::from_str(&format!(
"or(10@and(sha256({secret_hash}),pk({redeem_identity})),1@and(older({expiry}),pk({refund_identity})))",
secret_hash = self.hashlock,
redeem_identity = self.redeem_identity,
refund_identity = self.refund_indentiy,
expiry = self.refund_timelock
)).expect("Policy compilation only fails on resource limits or mixed timelocks")
}
}

View File

@@ -12,12 +12,6 @@ use std::{
path::PathBuf,
};
pub fn make_mnemonic() -> String {
let mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
Mnemonic::generate((WordCount::Words12, Language::English)).unwrap();
mnemonic.to_string()
}
/// Write the mnemonic to a file in the node's side directory
///
/// TODO: obviously spitting the mnemonic out to the console is not for production
@@ -30,8 +24,15 @@ pub(crate) fn write(mnemonic_path: &PathBuf) -> Result<(), std::io::Error> {
Ok(())
}
pub(crate) fn load_from_file(side_dir: &PathBuf) -> anyhow::Result<ExtendedKey> {
let mnemonic_path = crate::utils::side_paths(side_dir.clone()).1; // TODO: this tuple stinks
let mnemonic_words = fs::read_to_string(mnemonic_path).expect("couldn't read bitcoin key file");
println!("Creating extended key from mnemonic: {mnemonic_words}");
generate_extended_key(mnemonic_words)
}
/// Creates Signet Bitcoin descriptors from a mnemonic
pub fn get(mnemonic_words: String) -> anyhow::Result<ExtendedKey> {
fn generate_extended_key(mnemonic_words: String) -> anyhow::Result<ExtendedKey> {
let mnemonic = Mnemonic::parse(mnemonic_words).unwrap();
// Generate the extended key
@@ -42,9 +43,8 @@ pub fn get(mnemonic_words: String) -> anyhow::Result<ExtendedKey> {
Ok(xkey)
}
pub(crate) fn load_from_file(side_dir: &PathBuf) -> anyhow::Result<ExtendedKey> {
let mnemonic_path = crate::utils::side_paths(side_dir.clone()).1; // TODO: this tuple stinks
let mnemonic_words = fs::read_to_string(mnemonic_path).expect("couldn't read bitcoin key file");
println!("Creating wallet from mnemonic: {mnemonic_words}");
get(mnemonic_words)
fn make_mnemonic() -> String {
let mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
Mnemonic::generate((WordCount::Words12, Language::English)).unwrap();
mnemonic.to_string()
}

View File

@@ -1,3 +1,4 @@
pub mod clients;
pub mod client;
pub mod driver;
pub mod htlc;
pub mod keys;

View File

@@ -17,8 +17,14 @@ pub(crate) struct Args {
#[derive(Subcommand)]
pub(crate) enum Commands {
/// Placeholder for future BTC commands
Btc {},
/// transfers bitcoin between two wallets using a driver program
BtcTransfer {},
/// transfers bitcoin but this time uses a Miniscript policy
BtcPolicyTransfer {},
/// sets up a Bitcoin HTLC
BtcHtlc {},
/// runs the Side Node
Run { name: String },

View File

@@ -28,9 +28,18 @@ pub async fn run() {
let mut node = setup(name).await;
node.start().await;
}
Some(Commands::Btc {}) => {
let _ = bitcoin::driver::run().await;
Some(Commands::BtcTransfer {}) => {
let _ = bitcoin::driver::simple_transfer::run().await;
}
Some(Commands::BtcPolicyTransfer {}) => {
let _ = bitcoin::driver::policy_transfer::run().await;
}
Some(Commands::BtcHtlc {}) => {
let _ = bitcoin::driver::create_htlc::run().await;
}
None => println!("No command provided. Exiting. See --help for more information."),
}
}
@@ -40,8 +49,9 @@ async fn setup(name: &String) -> SideNode {
// First, load up the keys and create a bft-bft-crdt
let side_dir = utils::home(name);
let bft_crdt_keys = bft_crdt::keys::load_from_file(&side_dir);
let keys = bitcoin::keys::load_from_file(&side_dir).unwrap();
let bitcoin_wallet = bitcoin::clients::electrum::create_wallet(keys).unwrap();
// let keys = bitcoin::keys::load_from_file(&side_dir).unwrap();
// let bitcoin_wallet =
// bitcoin::clients::esplora::EsploraWallet::create_wallet(name, keys).unwrap();
let crdt = BaseCrdt::<TransactionList>::new(&bft_crdt_keys);
// Channels for internal communication, and a tokio task for stdin input
@@ -56,7 +66,7 @@ async fn setup(name: &String) -> SideNode {
let node = SideNode::new(
crdt,
bft_crdt_keys,
bitcoin_wallet,
// bitcoin_wallet,
incoming_receiver,
stdin_receiver,
handle,

View File

@@ -1,4 +1,3 @@
use bdk::database::MemoryDatabase;
use bft_json_crdt::json_crdt::{BaseCrdt, SignedOp};
use fastcrypto::ed25519::Ed25519KeyPair;
use tokio::sync::mpsc;
@@ -8,7 +7,7 @@ use crate::{bft_crdt::websocket::Client, bft_crdt::TransactionList, utils};
pub struct SideNode {
crdt: BaseCrdt<TransactionList>,
bft_crdt_keys: fastcrypto::ed25519::Ed25519KeyPair,
_bitcoin_wallet: bdk::Wallet<MemoryDatabase>, // currently not read anywhere
// _bitcoin_wallet: bdk::Wallet<MemoryDatabase>, // currently not read anywhere
incoming_receiver: mpsc::Receiver<SignedOp>,
stdin_receiver: std::sync::mpsc::Receiver<String>,
handle: ezsockets::Client<Client>,
@@ -18,7 +17,7 @@ impl SideNode {
pub fn new(
crdt: BaseCrdt<TransactionList>,
bft_crdt_keys: Ed25519KeyPair,
bitcoin_wallet: bdk::Wallet<MemoryDatabase>,
// bitcoin_wallet: bdk::Wallet<MemoryDatabase>,
incoming_receiver: mpsc::Receiver<SignedOp>,
stdin_receiver: std::sync::mpsc::Receiver<String>,
handle: ezsockets::Client<Client>,
@@ -26,7 +25,7 @@ impl SideNode {
let node = Self {
crdt,
bft_crdt_keys,
_bitcoin_wallet: bitcoin_wallet,
// _bitcoin_wallet: bitcoin_wallet,
incoming_receiver,
stdin_receiver,
handle,

View File

@@ -42,7 +42,7 @@ pub fn shappy(op: SignedOp) -> String {
sha256::digest(b).to_string()
}
pub fn shassy(text: String) -> String {
pub fn sha256(text: String) -> String {
let b = text.into_bytes();
sha256::digest(b).to_string()
}

View File

@@ -2,9 +2,7 @@ use bft_json_crdt::{
json_crdt::{BaseCrdt, SignedOp},
keypair::make_keypair,
};
use side_node::{
bft_crdt::websocket::Client, bft_crdt::TransactionList, bitcoin, node::SideNode, utils,
};
use side_node::{bft_crdt::websocket::Client, bft_crdt::TransactionList, node::SideNode, utils};
use tokio::sync::mpsc;
#[tokio::test]
@@ -37,9 +35,9 @@ async fn test_distribute_via_websockets() {
async fn setup(_: &str) -> SideNode {
// First, load up the keys and create a bft-bft-crdt
let bft_crdt_keys = make_keypair();
let mnemonic_words = bitcoin::keys::make_mnemonic();
let keys = bitcoin::keys::get(mnemonic_words).unwrap();
let bitcoin_wallet = bitcoin::clients::electrum::create_wallet(keys).unwrap();
// let mnemonic_words = bitcoin::keys::make_mnemonic();
// let keys = bitcoin::keys::get(mnemonic_words).unwrap();
// let bitcoin_wallet = bitcoin::clients::electrum::create_wallet(keys).unwrap();
let crdt = BaseCrdt::<TransactionList>::new(&bft_crdt_keys);
// Channels for internal communication, and a tokio task for stdin input
@@ -51,7 +49,7 @@ async fn setup(_: &str) -> SideNode {
let node = SideNode::new(
crdt,
bft_crdt_keys,
bitcoin_wallet,
// bitcoin_wallet,
incoming_receiver,
stdin_receiver,
handle,