Skip to content

Commit

Permalink
Merge pull request #522 from interlay/fix/sweep-funds
Browse files Browse the repository at this point in the history
fix: sweep funds into shared wallet
  • Loading branch information
gregdhill authored Sep 13, 2023
2 parents 06fd50b + 7134856 commit 1986c64
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 30 deletions.
5 changes: 5 additions & 0 deletions bitcoin/src/electrs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ impl ElectrsClient {
Ok(ret)
}

pub async fn is_tx_output_spent(&self, txid: &Txid, vout: u32) -> Result<bool, Error> {
let spending_value: SpendingValue = self.get_and_decode(&format!("/tx/{txid}/outspend/{vout}")).await?;
Ok(spending_value.spent)
}

pub async fn get_blocks_tip_height(&self) -> Result<u32, Error> {
Ok(self.get("/blocks/tip/height").await?.parse()?)
}
Expand Down
12 changes: 12 additions & 0 deletions bitcoin/src/electrs/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,15 @@ pub struct UtxoValue {
pub status: TransactionStatus,
pub value: u64,
}

// https://github.com/Blockstream/electrs/blob/adedee15f1fe460398a7045b292604df2161adc0/src/rest.rs#L448-L457
#[derive(Deserialize)]
pub struct SpendingValue {
pub spent: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub txid: Option<Txid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vin: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<TransactionStatus>,
}
3 changes: 3 additions & 0 deletions bitcoin/src/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,8 @@ mod tests {
async fn get_proof(&self, txid: Txid, block_hash: &BlockHash) -> Result<Vec<u8>, Error>;
async fn get_block_hash(&self, height: u32) -> Result<BlockHash, Error>;
async fn get_new_address(&self) -> Result<Address, Error>;
async fn get_new_sweep_address(&self) -> Result<Address, Error>;
async fn get_last_sweep_height(&self) -> Result<Option<u32>, Error>;
async fn get_new_public_key(&self) -> Result<PublicKey, Error>;
fn dump_private_key(&self, address: &Address) -> Result<PrivateKey, Error>;
fn import_private_key(&self, private_key: &PrivateKey, is_derivation_key: bool) -> Result<(), Error>;
Expand Down Expand Up @@ -251,6 +253,7 @@ mod tests {
fee_rate: SatPerVbyte,
num_confirmations: u32,
) -> Result<TransactionMetadata, Error>;
async fn sweep_funds(&self, address: Address) -> Result<Txid, Error>;
async fn create_or_load_wallet(&self) -> Result<(), Error>;
async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), Error>;
async fn rescan_electrs_for_addresses(
Expand Down
108 changes: 89 additions & 19 deletions bitcoin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ const RANDOMIZATION_FACTOR: f64 = 0.25;
const DERIVATION_KEY_LABEL: &str = "derivation-key";
const DEPOSIT_LABEL: &str = "deposit";

const SWEEP_ADDRESS: &str = "sweep-address";

fn get_exponential_backoff() -> ExponentialBackoff {
ExponentialBackoff {
current_interval: INITIAL_INTERVAL,
Expand Down Expand Up @@ -160,6 +162,10 @@ pub trait BitcoinCoreApi {

async fn get_new_address(&self) -> Result<Address, Error>;

async fn get_new_sweep_address(&self) -> Result<Address, Error>;

async fn get_last_sweep_height(&self) -> Result<Option<u32>, Error>;

async fn get_new_public_key(&self) -> Result<PublicKey, Error>;

fn dump_private_key(&self, address: &Address) -> Result<PrivateKey, Error>;
Expand Down Expand Up @@ -207,6 +213,8 @@ pub trait BitcoinCoreApi {
num_confirmations: u32,
) -> Result<TransactionMetadata, Error>;

async fn sweep_funds(&self, address: Address) -> Result<Txid, Error>;

async fn create_or_load_wallet(&self) -> Result<(), Error>;

async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), Error>;
Expand Down Expand Up @@ -362,7 +370,7 @@ impl BitcoinCoreBuilder {

#[derive(Clone)]
pub struct BitcoinCore {
rpc: Arc<Client>,
pub rpc: Arc<Client>,
wallet_name: Option<String>,
network: Network,
transaction_creation_lock: Arc<Mutex<()>>,
Expand Down Expand Up @@ -791,6 +799,26 @@ impl BitcoinCoreApi for BitcoinCore {
.require_network(self.network)?)
}

async fn get_new_sweep_address(&self) -> Result<Address, Error> {
Ok(self
.rpc
.get_new_address(Some(SWEEP_ADDRESS), Some(AddressType::Bech32))?
.require_network(self.network)?)
}

async fn get_last_sweep_height(&self) -> Result<Option<u32>, Error> {
Ok(self
.rpc
.list_transactions(Some(SWEEP_ADDRESS), Some(DEFAULT_MAX_TX_COUNT), None, None)?
.into_iter()
// we want to return None if there is no sweep tx for full nodes or new
// pruned nodes and we should return an error if any tx is still in the mempool
.map(|tx| tx.info.blockheight.ok_or(Error::ConfirmationError))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.min())
}

/// Gets a new public key for an address in the wallet
async fn get_new_public_key(&self) -> Result<PublicKey, Error> {
let address = self
Expand Down Expand Up @@ -1035,6 +1063,66 @@ impl BitcoinCoreApi for BitcoinCore {
.await?)
}

async fn sweep_funds(&self, address: Address) -> Result<Txid, Error> {
let unspent = self.rpc.list_unspent(Some(0), None, None, None, None)?;

let mut amount = Amount::ZERO;
let mut utxos = Vec::<json::CreateRawTransactionInput>::new();

for entry in unspent {
if self.electrs_client.is_tx_output_spent(&entry.txid, entry.vout).await? {
log::info!("{}:{} already spent", entry.txid, entry.vout);
// skip if already spent
continue;
}
amount += entry.amount;
utxos.push(json::CreateRawTransactionInput {
txid: entry.txid,
vout: entry.vout,
sequence: None,
})
}

log::info!("Sweeping {} from {} utxos", amount, utxos.len());
let mut outputs = serde_json::Map::<String, serde_json::Value>::new();
outputs.insert(address.to_string(), serde_json::Value::from(amount.to_btc()));

let args = [
serde_json::to_value::<&[json::CreateRawTransactionInput]>(&utxos)?,
serde_json::to_value(outputs)?,
serde_json::to_value(0i64)?, /* locktime - default 0: see https://developer.bitcoin.org/reference/rpc/createrawtransaction.html */
serde_json::to_value(true)?, // BIP125-replaceable, aka Replace By Fee (RBF)
];
let raw_tx: String = self.rpc.call("createrawtransaction", &args)?;

let funding_opts = FundRawTransactionOptions {
fee_rate: None,
add_inputs: Some(false),
subtract_fee_from_outputs: Some(vec![0]),
..Default::default()
};
let funded_raw_tx = self.rpc.fund_raw_transaction(raw_tx, Some(&funding_opts), None)?;

let signed_funded_raw_tx =
self.rpc
.sign_raw_transaction_with_wallet(&funded_raw_tx.transaction()?, None, None)?;

if signed_funded_raw_tx.errors.is_some() {
log::warn!(
"Received bitcoin funding errors (complete={}): {:?}",
signed_funded_raw_tx.complete,
signed_funded_raw_tx.errors
);
return Err(Error::TransactionSigningError);
}

let transaction = signed_funded_raw_tx.transaction()?;
let txid = self.rpc.send_raw_transaction(&transaction)?;
log::info!("Sent sweep tx: {txid}");

Ok(txid)
}

/// Create or load a wallet on Bitcoin Core.
async fn create_or_load_wallet(&self) -> Result<(), Error> {
let wallet_name = if let Some(ref wallet_name) = self.wallet_name {
Expand Down Expand Up @@ -1118,24 +1206,6 @@ impl BitcoinCoreApi for BitcoinCore {
&[serde_json::to_value(raw_tx)?, serde_json::to_value(raw_merkle_proof)?],
)?;
}
// TODO: remove this migration after the next runtime upgrade
// filter to remove spent funds, the previous wallet migration caused
// signing failures for pruned nodes because they tried to double spend
let confirmed_payments_from = all_transactions.iter().filter(|tx| {
if let Some(status) = &tx.status {
if !status.confirmed {
return false;
}
};
tx.vin
.iter()
.filter_map(|input| input.prevout.clone())
.any(|output| matches!(&output.scriptpubkey_address, Some(addr) if addr == &address))
});
for transaction in confirmed_payments_from {
self.rpc
.call("removeprunedfunds", &[serde_json::to_value(transaction.txid)?])?;
}
}
Ok(())
}
Expand Down
12 changes: 12 additions & 0 deletions bitcoin/src/light/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ impl BitcoinCoreApi for BitcoinLight {
Ok(self.get_change_address()?)
}

async fn get_new_sweep_address(&self) -> Result<Address, BitcoinError> {
Ok(self.get_change_address()?)
}

async fn get_last_sweep_height(&self) -> Result<Option<u32>, BitcoinError> {
Ok(None)
}

async fn get_new_public_key(&self) -> Result<PublicKey, BitcoinError> {
Ok(self.private_key.public_key(&self.secp_ctx))
}
Expand Down Expand Up @@ -331,6 +339,10 @@ impl BitcoinCoreApi for BitcoinLight {
.await?)
}

async fn sweep_funds(&self, _address: Address) -> Result<Txid, BitcoinError> {
Ok(Txid::all_zeros())
}

async fn create_or_load_wallet(&self) -> Result<(), BitcoinError> {
// nothing to do
Ok(())
Expand Down
55 changes: 54 additions & 1 deletion bitcoin/tests/integration_test.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
#![cfg(feature = "uses-bitcoind")]

use bitcoin::{Auth, BitcoinCore, BitcoinCoreApi, BitcoinCoreBuilder, Error, Network, PrivateKey, PublicKey};
use bitcoin::{
Amount, Auth, BitcoinCore, BitcoinCoreApi, BitcoinCoreBuilder, Error, Network, PrivateKey, PublicKey, RpcApi,
};
use bitcoincore_rpc::json;
use rand::{distributions::Alphanumeric, Rng};
use regex::Regex;
use std::env::var;

Expand Down Expand Up @@ -89,3 +93,52 @@ async fn should_add_new_deposit_key() -> Result<(), Error> {

Ok(())
}

fn rand_wallet_name() -> String {
rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(7)
.map(char::from)
.collect()
}

#[tokio::test]
async fn should_sweep_funds() -> Result<(), Error> {
let btc_rpc1 = new_bitcoin_core(Some(rand_wallet_name()))?;
btc_rpc1.create_or_load_wallet().await?;

let btc_rpc2 = new_bitcoin_core(Some(rand_wallet_name()))?;
btc_rpc2.create_or_load_wallet().await?;

let btc_rpc3 = new_bitcoin_core(Some(rand_wallet_name()))?;
btc_rpc3.create_or_load_wallet().await?;

// blocks must have 100 confirmations for reward to be spent
let address1 = btc_rpc1.get_new_address().await?;
btc_rpc1.mine_blocks(101, Some(address1));

let address2 = btc_rpc2.get_new_address().await?;
let txid = btc_rpc1.rpc.send_to_address(
&address2,
Amount::from_sat(100000),
None,
None,
None,
None,
None,
Some(json::EstimateMode::Economical),
)?;
btc_rpc1.mine_blocks(1, None);

assert_eq!(btc_rpc2.get_balance(None)?.to_sat(), 100000);

let address3 = btc_rpc3.get_new_address().await?;
let _txid = btc_rpc2.sweep_funds(address3).await?;
btc_rpc1.mine_blocks(1, None);

assert_eq!(btc_rpc2.get_balance(None)?.to_sat(), 0);
// balance before minus fees
assert_eq!(btc_rpc3.get_balance(None)?.to_sat(), 97800);

Ok(())
}
13 changes: 13 additions & 0 deletions runtime/src/integration/bitcoin_simulator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,15 @@ impl BitcoinCoreApi for MockBitcoinCore {
let address = BtcAddress::P2PKH(H160::from(bytes));
Ok(address.to_address(Network::Regtest)?)
}

async fn get_new_sweep_address(&self) -> Result<Address, BitcoinError> {
self.get_new_address().await
}

async fn get_last_sweep_height(&self) -> Result<Option<u32>, BitcoinError> {
Ok(None)
}

async fn get_new_public_key(&self) -> Result<PublicKey, BitcoinError> {
let secp = Secp256k1::new();
let raw_secret_key: [u8; SECRET_KEY_SIZE] = thread_rng().gen();
Expand Down Expand Up @@ -514,6 +523,9 @@ impl BitcoinCoreApi for MockBitcoinCore {
.unwrap();
Ok(metadata)
}
async fn sweep_funds(&self, _address: Address) -> Result<Txid, BitcoinError> {
Ok(Txid::all_zeros())
}
async fn create_or_load_wallet(&self) -> Result<(), BitcoinError> {
Ok(())
}
Expand All @@ -524,6 +536,7 @@ impl BitcoinCoreApi for MockBitcoinCore {
async fn rescan_electrs_for_addresses(&self, addresses: Vec<Address>) -> Result<(), BitcoinError> {
Ok(())
}

fn get_utxo_count(&self) -> Result<usize, BitcoinError> {
Ok(0)
}
Expand Down
5 changes: 5 additions & 0 deletions vault/src/connection_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub trait Service<Config> {
btc_parachain: BtcParachain,
bitcoin_core_master: DynBitcoinCoreApi,
bitcoin_core_shared: DynBitcoinCoreApi,
bitcoin_core_shared_v2: DynBitcoinCoreApi,
config: Config,
monitoring_config: MonitoringConfig,
shutdown: ShutdownSender,
Expand Down Expand Up @@ -107,6 +108,9 @@ impl<Config: Clone + Send + 'static, F: Fn()> ConnectionManager<Config, F> {
let bitcoin_core_shared =
config_copy.new_client_with_network(Some(format!("{prefix}-shared")), network_copy)?;
bitcoin_core_shared.create_or_load_wallet().await?;
let bitcoin_core_shared_v2 =
config_copy.new_client_with_network(Some(format!("{prefix}-shared-v2")), network_copy)?;
bitcoin_core_shared_v2.create_or_load_wallet().await?;

let constructor = move |vault_id: VaultId| {
let collateral_currency: CurrencyId = vault_id.collateral_currency();
Expand All @@ -128,6 +132,7 @@ impl<Config: Clone + Send + 'static, F: Fn()> ConnectionManager<Config, F> {
btc_parachain,
bitcoin_core_master,
bitcoin_core_shared,
bitcoin_core_shared_v2,
config,
self.monitoring_config.clone(),
shutdown_tx.clone(),
Expand Down
3 changes: 3 additions & 0 deletions vault/src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,8 @@ mod tests {
async fn get_block_hash(&self, height: u32) -> Result<BlockHash, BitcoinError>;
async fn get_pruned_height(&self) -> Result<u64, BitcoinError>;
async fn get_new_address(&self) -> Result<Address, BitcoinError>;
async fn get_new_sweep_address(&self) -> Result<Address, BitcoinError>;
async fn get_last_sweep_height(&self) -> Result<Option<u32>, BitcoinError>;
async fn get_new_public_key(&self) -> Result<PublicKey, BitcoinError>;
fn dump_private_key(&self, address: &Address) -> Result<PrivateKey, BitcoinError>;
fn import_private_key(&self, private_key: &PrivateKey, is_derivation_key: bool) -> Result<(), BitcoinError>;
Expand All @@ -820,6 +822,7 @@ mod tests {
async fn wait_for_transaction_metadata(&self, txid: Txid, num_confirmations: u32, block_hash: Option<BlockHash>, is_wallet: bool) -> Result<TransactionMetadata, BitcoinError>;
async fn create_and_send_transaction(&self, address: Address, sat: u64, fee_rate: SatPerVbyte, request_id: Option<H256>) -> Result<Txid, BitcoinError>;
async fn send_to_address(&self, address: Address, sat: u64, request_id: Option<H256>, fee_rate: SatPerVbyte, num_confirmations: u32) -> Result<TransactionMetadata, BitcoinError>;
async fn sweep_funds(&self, address: Address) -> Result<Txid, BitcoinError>;
async fn create_or_load_wallet(&self) -> Result<(), BitcoinError>;
async fn rescan_blockchain(&self, start_height: usize, end_height: usize) -> Result<(), BitcoinError>;
async fn rescan_electrs_for_addresses(&self, addresses: Vec<Address>) -> Result<(), BitcoinError>;
Expand Down
Loading

0 comments on commit 1986c64

Please sign in to comment.