Implement custom script subscribe for btc

This commit is contained in:
Janus
2021-10-21 18:13:26 -04:00
parent 4c6a1ff640
commit 05d036c642
3 changed files with 462 additions and 67 deletions

90
Cargo.lock generated
View File

@@ -420,6 +420,46 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "base64-compat"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7"
dependencies = [
"byteorder",
]
[[package]]
name = "bdk"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecf7e997526ceefbab7dd99fc0da6834ed8853bd051f53523415ed1dc82b870d"
dependencies = [
"async-trait",
"bdk-macros",
"bitcoin",
"electrum-client",
"js-sys",
"log",
"miniscript",
"rand 0.7.3",
"serde",
"serde_json",
"sled",
"tokio",
]
[[package]]
name = "bdk-macros"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81c1980e50ae23bb6efa9283ae8679d6ea2c6fa6a99fe62533f65f4a25a1a56c"
dependencies = [
"proc-macro2 1.0.30",
"quote 1.0.10",
"syn 1.0.80",
]
[[package]]
name = "bech32"
version = "0.8.1"
@@ -486,6 +526,7 @@ version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a41df6ad9642c5c15ae312dd3d074de38fd3eb7cc87ad4ce10f90292a83fe4d"
dependencies = [
"base64-compat",
"bech32",
"bitcoin_hashes",
"secp256k1",
@@ -1255,16 +1296,17 @@ dependencies = [
name = "darkfi"
version = "0.1.0"
dependencies = [
"anyhow",
"async-channel",
"async-executor",
"async-native-tls",
"async-std",
"async-trait",
"async-tungstenite",
"bdk",
"bellman",
"bimap",
"bitcoin",
"bitcoin_hashes",
"bitvec 0.18.5",
"blake2b_simd",
"blake2s_simd",
@@ -1275,7 +1317,6 @@ dependencies = [
"crypto_api_chachapoly",
"dirs 4.0.0",
"easy-parallel",
"electrum-client",
"ff",
"futures 0.3.17",
"group",
@@ -1715,6 +1756,16 @@ dependencies = [
"num-traits",
]
[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
dependencies = [
"libc",
"winapi 0.3.9",
]
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
@@ -1852,6 +1903,15 @@ dependencies = [
"slab",
]
[[package]]
name = "fxhash"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
dependencies = [
"byteorder",
]
[[package]]
name = "generic-array"
version = "0.12.4"
@@ -2507,6 +2567,16 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "miniscript"
version = "6.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d69450033bf162edf854d4aacaff82ca5ef34fa81f6cf69e1c81a103f0834997"
dependencies = [
"bitcoin",
"serde",
]
[[package]]
name = "miniz_oxide"
version = "0.4.4"
@@ -3767,6 +3837,22 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
[[package]]
name = "sled"
version = "0.34.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
dependencies = [
"crc32fast",
"crossbeam-epoch 0.9.5",
"crossbeam-utils 0.8.5",
"fs2",
"fxhash",
"libc",
"log",
"parking_lot 0.11.2",
]
[[package]]
name = "smallvec"
version = "1.7.0"

View File

@@ -70,16 +70,16 @@ spl-token = {version = "3.2.0", features = ["no-entrypoint"], optional = true}
spl-associated-token-account = {version = "1.0.3", features = ["no-entrypoint"], optional = true}
## Cashier Bitcoin Dependencies
bitcoin = {version = "0.27.0", optional = true}
bitcoin_hashes = "0.10.0"
anyhow = "1.0.44"
bdk = {version = "0.12.0", optional = true}
bitcoin = {version = "0.27.0", optional = true }
secp256k1 = {version = "0.20.3", default-features = false, features = ["rand-std"], optional = true}
electrum-client = {version = "0.8.0", optional = true}
## Cashier Ethereum Dependencies
hash-db = {version = "0.15.2", optional = true}
keccak-hasher = {version = "0.15.3", optional = true}
[features]
btc = ["bitcoin", "secp256k1", "electrum-client"]
btc = ["bdk", "bitcoin", "secp256k1"]
sol = ["solana-sdk", "solana-client", "spl-token", "spl-associated-token-account"]
eth = ["keccak-hasher", "hash-db"]

View File

@@ -1,8 +1,13 @@
use async_std::sync::Arc;
use std::convert::From;
use async_std::sync::{Arc, Mutex};
use std::collections::{BTreeMap, HashMap};
use std::convert::{From, TryFrom, TryInto};
use std::cmp::max;
use std::fmt;
use std::ops::Add;
use std::str::FromStr;
use std::time::Duration;
use std::time::{Duration, Instant};
use anyhow::Context;
use async_executor::Executor;
use async_trait::async_trait;
@@ -11,12 +16,18 @@ use bitcoin::blockdata::{
transaction::{OutPoint, SigHashType, Transaction, TxIn, TxOut},
};
use bitcoin::consensus::encode::serialize_hex;
use bitcoin::hash_types::PubkeyHash as BtcPubKeyHash;
use bitcoin::hash_types::{PubkeyHash as BtcPubKeyHash, Txid};
use bitcoin::network::constants::Network;
use bitcoin::util::address::Address;
use bitcoin::util::ecdsa::{PrivateKey as BtcPrivKey, PublicKey as BtcPubKey};
use bitcoin::util::psbt::serialize::Serialize;
use electrum_client::{Client as ElectrumClient, ElectrumApi, GetBalanceRes};
use bdk::electrum_client::{
Client as ElectrumClient,
ElectrumApi,
GetBalanceRes,
GetHistoryRes,
HeaderNotification
};
use log::*;
use secp256k1::{
constants::{PUBLIC_KEY_SIZE, SECRET_KEY_SIZE},
@@ -37,6 +48,40 @@ pub type PrivKey = BtcPrivKey;
const KEYPAIR_LENGTH: usize = SECRET_KEY_SIZE + PUBLIC_KEY_SIZE;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct BlockHeight(u32);
impl From<BlockHeight> for u32 {
fn from(height: BlockHeight) -> Self {
height.0
}
}
impl TryFrom<HeaderNotification> for BlockHeight {
type Error = BtcFailed;
fn try_from(value: HeaderNotification) -> BtcResult<Self> {
Ok(Self(
value
.height
.try_into()
.context("Failed to fit usize into u32")?,
))
}
}
impl Add<u32> for BlockHeight {
type Output = BlockHeight;
fn add(self, rhs: u32) -> Self::Output {
BlockHeight(self.0 + rhs)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ExpiredTimelocks {
None,
Cancel,
Punish,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Keypair {
secret: SecretKey,
@@ -155,17 +200,163 @@ impl Account {
Script::new_p2pkh(&btc_pubkey_hash)
}
}
fn print_status_change(txid: Txid, old: Option<ScriptStatus>, new: ScriptStatus) -> ScriptStatus {
match (old, new) {
(None, new_status) => {
debug!(target: "BTC BRIDGE", "Found relevant Bitcoin transaction: {:?} {:?}", txid, new_status);
}
(Some(old_status), new_status) if old_status != new_status => {
debug!(target: "BTC BRIDGE", "Bitcoin transaction status changed: {:?} {} {}", txid, new_status, old_status);
}
_ => {}
}
new
}
fn sync_interval(avg_block_time: Duration) -> Duration {
max(avg_block_time / 10, Duration::from_secs(1))
}
pub struct Client {
electrum: ElectrumClient,
subscriptions: Arc<Mutex<Vec<Script>>>,
latest_block_height: BlockHeight,
last_sync: Instant,
sync_interval: Duration,
script_history: BTreeMap<Script, Vec<GetHistoryRes>>,
}
impl Client {
pub fn new(electrum_url: &str) -> BtcResult<Self> {
let config = bdk::electrum_client::ConfigBuilder::default()
.retry(5)
.build();
let client = ElectrumClient::from_config(electrum_url, config)?;
let electrum = ElectrumClient::new(electrum_url)
.map_err(|err| crate::Error::from(super::BtcFailed::from(err)))?;
let latest_block = electrum
.block_headers_subscribe()?;
//testnet avg block time
let interval = sync_interval(Duration::from_secs(300));
Ok(Self {
electrum: electrum,
subscriptions: Arc::new(Mutex::new(Vec::new())),
latest_block_height: BlockHeight::try_from(latest_block)
.map_err(|_| crate::Error::TryFromError)?,
last_sync: Instant::now(),
sync_interval: interval,
script_history: Default::default(),
})
}
fn update_state(&mut self) -> Result<()> {
let now = Instant::now();
if now < self.last_sync + self.sync_interval {
return Ok(());
}
self.last_sync = now;
self.update_latest_block()?;
self.update_script_histories()?;
Ok(())
}
fn update_latest_block(&mut self) -> BtcResult<()> {
let latest_block = self
.electrum
.block_headers_subscribe()?;
let latest_block_height = BlockHeight::try_from(latest_block)
.map_err(|err| crate::Error::from(super::BtcFailed::from(err)))?;
if latest_block_height > self.latest_block_height {
// debug!( target: "BTC BRIDGE", "{} {}"
// u32::from(latest_block_height),
// "Got notification for new block"
// );
self.latest_block_height = latest_block_height;
}
Ok(())
}
fn update_script_histories(&mut self) -> BtcResult<()> {
let histories = self
.electrum
.batch_script_get_history(self.script_history.keys())?;
if histories.len() != self.script_history.len() {
debug!(
"Expected {} history entries, received {}",
self.script_history.len(),
histories.len()
);
}
let scripts = self.script_history.keys().cloned();
let histories = histories.into_iter();
self.script_history = scripts.zip(histories).collect::<BTreeMap<_, _>>();
Ok(())
}
pub fn status_of_script<T>(&mut self, tx: &T) -> BtcResult<ScriptStatus>
where
T: Watchable,
{
let txid = tx.id();
let script = tx.script();
if !self.script_history.contains_key(&script) {
self.script_history.insert(script.clone(), vec![]);
}
self.update_state()?;
let history = self.script_history.entry(script).or_default();
let history_of_tx = history
.iter()
.filter(|entry| entry.tx_hash == txid)
.collect::<Vec<_>>();
match history_of_tx.as_slice() {
[] => Ok(ScriptStatus::Unseen),
[remaining @ .., last] => {
if !remaining.is_empty() {
debug!("Found more than a single history entry for script. This is highly unexpected and those history entries will be ignored")
}
if last.height <= 0 {
Ok(ScriptStatus::InMempool)
} else {
Ok(ScriptStatus::Confirmed(
Confirmed::from_inclusion_and_latest_block(
u32::try_from(last.height)
.map_err(|_| crate::Error::TryFromError)?,
u32::from(self.latest_block_height),
),
))
}
}
}
}
}
pub struct BtcClient {
main_account: Account,
client: Arc<Mutex<Client>>,
notify_channel: (
async_channel::Sender<TokenNotification>,
async_channel::Receiver<TokenNotification>,
),
client: Arc<ElectrumClient>,
network: Network,
}
impl BtcClient {
pub async fn new(main_keypair: Keypair, network: &str) -> Result<Arc<Self>> {
//TODO
@@ -181,19 +372,18 @@ impl BtcClient {
let main_account = Account::new(&main_keypair, network);
let electrum_client = ElectrumClient::new(url)
.map_err(|err| crate::Error::from(super::BtcFailed::from(err)))?;
Ok(Arc::new(Self {
main_account,
client: Arc::new(Mutex::new(Client::new(url)?)),
notify_channel,
client: Arc::new(electrum_client),
network,
}))
}
async fn handle_subscribe_request(
self: Arc<Self>,
tx: impl Watchable + Send + 'static,
btc_keys: Account,
drk_pub_key: jubjub::SubgroupPoint,
) -> BtcResult<()> {
@@ -201,51 +391,54 @@ impl BtcClient {
target: "BTC BRIDGE",
"Handle subscribe request"
);
let client = &self.client;
let keys_clone = btc_keys.clone();
// p2pkh script
let script = keys_clone.script_pubkey;
let client = self.client.clone();
let electrum = &client.lock().await.electrum;
let script = tx.script();
let txid = tx.id();
// Check if we're already subscribed
if client.lock().await
.subscriptions.lock().await.contains(&script) {
return Ok(());
}
//Fetch any current balance
let prev_balance = client.script_get_balance(&script)?;
let prev_balance = electrum.script_get_balance(&script)?;
let cur_balance: GetBalanceRes;
//let status = client.script_subscribe(&script)?;
let mut last_status = None;
let status = client.script_subscribe(&script)?;
loop {
let current_status = client.script_pop(&script)?;
debug!(target: "BTC BRIDGE", "script status: {:?}", status);
debug!(target: "BTC BRIDGE", "current_script status: {:?}", current_status);
if current_status == status {
async_std::task::sleep(Duration::from_secs(5)).await;
debug!(
target: "BTC BRIDGE",
"ScriptPubKey status has not changed, amtucfd: {}, amtcfd: {}",
client.script_get_balance(&script)?.unconfirmed,
client.script_get_balance(&script)?.confirmed
);
continue;
}
async_std::task::sleep(Duration::from_secs(5)).await;
//let current_status = client.script_pop(&script)?;
match current_status {
Some(_) => {
// Script has a notification update
debug!(target: "BTC BRIDGE", "ScripPubKey notify update");
//TODO: unsubscribe is never successful
//let _ = client.script_unsubscribe(&script)?;
break;
}
None => {
return Err(BtcFailed::ElectrumError(
"ScriptPubKey was not found".to_string(),
));
let new_status = match client.lock().await.status_of_script(&tx) {
Ok(new_status) => new_status,
Err(error) => {
debug!(target: "BTC BRIDGE", "Failed to get status of script: {:#}", error);
return Err(BtcFailed::BtcError("Failed to get status of script".to_string()));
}
};
last_status = Some(print_status_change(txid, last_status, new_status));
match new_status {
ScriptStatus::Unseen => continue,
ScriptStatus::InMempool => continue,
ScriptStatus::Confirmed(inner) => {
let confirmations = inner.confirmations();
debug!(target: "BTC BRIDGE", "Received confirmed tx: {:#}", confirmations);
if confirmations > 0 {
break
}
},
}
} // Endloop
cur_balance = client.script_get_balance(&script)?;
cur_balance = electrum.script_get_balance(&script)?;
let send_notification = self.notify_channel.0.clone();
@@ -271,17 +464,18 @@ impl BtcClient {
.map_err(Error::from)?;
debug!(target: "BTC BRIDGE", "Received {} btc", ui_amnt);
let _ = self.send_btc_to_main_wallet(amnt as u64, btc_keys)?;
let _ = self.send_btc_to_main_wallet(amnt as u64, btc_keys).await;
Ok(())
}
fn send_btc_to_main_wallet(self: Arc<Self>, amount: u64, btc_keys: Account) -> BtcResult<()> {
async fn send_btc_to_main_wallet(self: Arc<Self>, amount: u64, btc_keys: Account) -> BtcResult<()> {
debug!(target: "BTC BRIDGE", "Sending {} BTC to main wallet", amount);
let client = &self.client;
let client = self.client.lock().await;
let electrum = &client.electrum;
let keys_clone = btc_keys.clone();
let script = keys_clone.script_pubkey;
let utxo = client.script_list_unspent(&script)?;
let utxo = electrum.script_list_unspent(&script)?;
let mut inputs = Vec::new();
let mut amounts: u64 = 0;
@@ -311,13 +505,10 @@ impl BtcClient {
version: 2,
};
//TODO: Better handling of fees, don't cast to u64
let tx_size = transaction.get_size();
//Estimate fee for getting in next block
let fee_per_kb = client.estimate_fee(1)?;
let _fee = tx_size as f64 * fee_per_kb * 100000_f64;
//let value = amounts - fee as u64;
let transaction = Transaction {
input: inputs,
@@ -346,11 +537,12 @@ impl BtcClient {
debug!(target: "BTC BRIDGE", "Signed tx: {:?}",
serialize_hex(&signed_tx));
let txid = client.transaction_broadcast_raw(&signed_tx.serialize().to_vec())?;
let txid = electrum.transaction_broadcast_raw(&signed_tx.serialize().to_vec())?;
debug!(target: "BTC BRIDGE", "Sent {} satoshi to main wallet, txid: {}", amount, txid);
Ok(())
}
}
#[async_trait]
@@ -367,12 +559,22 @@ impl NetworkClient for BtcClient {
let private_key = serialize(&keypair);
let public_key = btc_keys.address.to_string();
let keys_clone = btc_keys.clone();
let script = keys_clone.script_pubkey;
let txid = self.client.lock().await.electrum
.script_get_history(&script).unwrap()[0].tx_hash;
// start scheduler for checking balance
debug!(target: "BRIDGE BITCOIN", "Subscribing for deposit");
executor
.spawn(async move {
let result = self.handle_subscribe_request(btc_keys, drk_pub_key).await;
let result = self.handle_subscribe_request(
(txid, script),
btc_keys,
drk_pub_key
).await;
if let Err(e) = result {
error!(target: "BTC BRIDGE SUBSCRIPTION","{}", e.to_string());
}
@@ -397,9 +599,20 @@ impl NetworkClient for BtcClient {
let btc_keys = Account::new(&keypair, self.network);
let public_key = btc_keys.address.to_string();
let keys_clone = btc_keys.clone();
let script = keys_clone.script_pubkey;
//Ugly
let txid = self.client.lock().await.electrum.
script_get_history(&script).unwrap()[0].tx_hash;
executor
.spawn(async move {
let result = self.handle_subscribe_request(btc_keys, drk_pub_key).await;
let result = self.handle_subscribe_request(
(txid, script),
btc_keys,
drk_pub_key
).await;
if let Err(e) = result {
error!(target: "BTC BRIDGE SUBSCRIPTION","{}", e.to_string());
}
@@ -418,14 +631,14 @@ impl NetworkClient for BtcClient {
amount: u64,
) -> Result<()> {
// address is not a btc address, so derive the btc address
let client = &self.client;
let electrum = &self.client.lock().await.electrum;
let public_key = deserialize(&address)?;
let script_pubkey = Account::derive_btc_script_pubkey(public_key, self.network);
let main_script_pubkey = &self.main_account.script_pubkey;
let main_utxo = client
.script_list_unspent(main_script_pubkey)
let main_utxo = electrum
.script_list_unspent(&main_script_pubkey)
.map_err(|e| Error::from(BtcFailed::from(e)))?;
let transaction = Transaction {
@@ -455,7 +668,7 @@ impl NetworkClient for BtcClient {
&self.main_account.keypair.context,
)?;
let txid = client
let txid = electrum
.transaction_broadcast_raw(&signed_tx.serialize().to_vec())
.map_err(|e| Error::from(BtcFailed::from(e)))?;
@@ -502,6 +715,97 @@ pub fn sign_transaction(
output: tx.output,
})
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum ScriptStatus {
Unseen,
InMempool,
Confirmed(Confirmed),
}
impl ScriptStatus {
pub fn from_confirmations(confirmations: u32) -> Self {
match confirmations {
0 => Self::InMempool,
confirmations => Self::Confirmed(Confirmed::new(confirmations - 1)),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Confirmed {
depth: u32,
}
impl Confirmed {
pub fn new(depth: u32) -> Self {
Self { depth }
}
pub fn from_inclusion_and_latest_block(inclusion_height: u32, latest_block: u32) -> Self {
let depth = latest_block.saturating_sub(inclusion_height);
Self { depth }
}
pub fn confirmations(&self) -> u32 {
self.depth + 1
}
pub fn meets_target<T>(&self, target: T) -> bool
where
u32: PartialOrd<T>,
{
self.confirmations() >= target
}
}
impl ScriptStatus {
// Check if the script has any confirmations.
pub fn is_confirmed(&self) -> bool {
matches!(self, ScriptStatus::Confirmed(_))
}
// Check if the script has met the given confirmation target.
pub fn is_confirmed_with<T>(&self, target: T) -> bool
where
u32: PartialOrd<T>,
{
match self {
ScriptStatus::Confirmed(inner) => inner.meets_target(target),
_ => false,
}
}
pub fn has_been_seen(&self) -> bool {
matches!(self, ScriptStatus::InMempool | ScriptStatus::Confirmed(_))
}
}
impl fmt::Display for ScriptStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ScriptStatus::Unseen => write!(f, "unseen"),
ScriptStatus::InMempool => write!(f, "in mempool"),
ScriptStatus::Confirmed(inner) => {
write!(f, "confirmed with {} blocks", inner.confirmations())
}
}
}
}
pub trait Watchable {
fn id(&self) -> Txid;
fn script(&self) -> Script;
}
impl Watchable for (Txid, Script) {
fn id(&self) -> Txid {
self.0
}
fn script(&self) -> Script {
self.1.clone()
}
}
impl Encodable for bitcoin::Transaction {
fn encode<S: std::io::Write>(&self, s: S) -> Result<usize> {
let tx = self.serialize();
@@ -605,7 +909,6 @@ pub enum BtcFailed {
KeypairError(String),
Notification(String),
}
impl std::error::Error for BtcFailed {}
impl std::fmt::Display for BtcFailed {
@@ -649,8 +952,8 @@ impl From<bitcoin::util::address::Error> for BtcFailed {
BtcFailed::BadBtcAddress(err.to_string())
}
}
impl From<electrum_client::Error> for BtcFailed {
fn from(err: electrum_client::Error) -> BtcFailed {
impl From<bdk::electrum_client::Error> for BtcFailed {
fn from(err: bdk::electrum_client::Error) -> BtcFailed {
BtcFailed::ElectrumError(err.to_string())
}
}
@@ -660,6 +963,12 @@ impl From<bitcoin::util::key::Error> for BtcFailed {
BtcFailed::DecodeAndEncodeError(err.to_string())
}
}
impl From<anyhow::Error> for BtcFailed {
fn from(err: anyhow::Error) -> BtcFailed {
BtcFailed::DecodeAndEncodeError(err.to_string())
}
}
pub type BtcResult<T> = std::result::Result<T, BtcFailed>;