diff --git a/Cargo.lock b/Cargo.lock index 16aefe3..59646b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3231,9 +3231,9 @@ dependencies = [ [[package]] name = "rgb-lib" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a390772538e403c588482107f768449d715428b0f0ae4619f675dec35cef64" +checksum = "11472a13ebe252b4519aa526953dc02d7183bbb7a1c3bf027d4f701fb68d0e90" dependencies = [ "amplify", "base64 0.21.4", diff --git a/Cargo.toml b/Cargo.toml index c913473..6064a66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ zip = { version = "0.6.6", default-features = false, features = ["time", "zstd"] # RGB-related deps amplify = "=4.5.0" bp-core = "=0.10.11" -rgb-lib = "=0.2.0" +rgb-lib = "=0.2.1" rgb-std = "=0.10.9" rgb-wallet = "=0.10.9" rgb_core = { package = "rgb-core", version = "=0.10.8" } diff --git a/openapi.yaml b/openapi.yaml index 4901763..f4a42a0 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1097,6 +1097,9 @@ components: public: type: boolean example: true + with_anchors: + type: boolean + example: true OpenChannelResponse: type: object properties: diff --git a/rust-lightning b/rust-lightning index 515063b..545591c 160000 --- a/rust-lightning +++ b/rust-lightning @@ -1 +1 @@ -Subproject commit 515063b265f9e0d3c0322e0fe217d329af8f519a +Subproject commit 545591cff389175a38e4c033201d5318c8654237 diff --git a/src/error.rs b/src/error.rs index 3448337..4376c79 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,6 +16,9 @@ pub enum APIError { #[error("Node has already been initialized")] AlreadyInitialized, + #[error("Anchor outputs are required for RGB channels")] + AnchorsRequired, + #[error("Cannot call other APIs while node is changing state")] ChangingState, @@ -168,7 +171,8 @@ impl IntoResponse for APIError { | APIError::IO(_) | APIError::Proxy(_) | APIError::Unexpected => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), - APIError::InvalidAmount(_) + APIError::AnchorsRequired + | APIError::InvalidAmount(_) | APIError::InvalidAssetID(_) | APIError::InvalidBackupPath | APIError::InvalidBlindedUTXO(_) diff --git a/src/ldk.rs b/src/ldk.rs index b947374..0295351 100644 --- a/src/ldk.rs +++ b/src/ldk.rs @@ -13,6 +13,7 @@ use bitcoin_30::{Address, ScriptBuf}; use bitcoin_bech32::WitnessProgram; use lightning::chain::{chainmonitor, ChannelMonitorUpdateStatus}; use lightning::chain::{Filter, Watch}; +use lightning::events::bump_transaction::{BumpTransactionEventHandler, Wallet}; use lightning::events::{Event, PaymentFailureReason, PaymentPurpose}; use lightning::ln::channelmanager::{self, PaymentId, RecentPaymentDetails}; use lightning::ln::channelmanager::{ @@ -70,7 +71,9 @@ use crate::disk::{self, INBOUND_PAYMENTS_FNAME, OUTBOUND_PAYMENTS_FNAME}; use crate::disk::{FilesystemLogger, PENDING_SPENDABLE_OUTPUT_DIR}; use crate::error::APIError; use crate::proxy::post_consignment; -use crate::rgb::{get_bitcoin_network, update_transition_beneficiary, RgbUtilities}; +use crate::rgb::{ + get_bitcoin_network, update_transition_beneficiary, RgbLibWalletWrapper, RgbUtilities, +}; use crate::routes::HTLCStatus; use crate::utils::{do_connect_peer, hex_str, AppState, StaticState, UnlockedAppState}; @@ -159,6 +162,13 @@ pub(crate) type NetworkGraph = gossip::NetworkGraph>; pub(crate) type OnionMessenger = SimpleArcOnionMessenger; +pub(crate) type BumpTxEventHandler = BumpTransactionEventHandler< + Arc, + Arc, Arc>>, + Arc, + Arc, +>; + async fn handle_ldk_events( event: Event, unlocked_state: Arc, @@ -224,7 +234,7 @@ async fn handle_ldk_events( let signed_psbt = unlocked_state .get_rgb_wallet() - .sign_psbt(unsigned_psbt) + .sign_psbt(unsigned_psbt, None) .unwrap(); let psbt = BdkPsbt::from_str(&signed_psbt).unwrap(); @@ -652,9 +662,7 @@ async fn handle_ldk_events( // the funding transaction either confirms, or this event is generated. } Event::HTLCIntercepted { .. } => {} - Event::BumpTransaction(_event) => { - unreachable!("BumpTxEventHandler needs to be implemented") - } + Event::BumpTransaction(event) => unlocked_state.bump_tx_event_handler.handle_event(&event), } } @@ -667,6 +675,21 @@ async fn _spend_outputs( let output_descriptors = &outputs.iter().collect::>(); let tx_feerate = FEE_RATE as u32 * 250; // 1 sat/vB = 250 sat/kw + // We set nLockTime to the current height to discourage fee sniping. + // Occasionally randomly pick a nLockTime even further back, so + // that transactions that are delayed after signing for whatever reason, + // e.g. high-latency mix networks and some CoinJoin implementations, have + // better privacy. + // Logic copied from core: https://github.com/bitcoin/bitcoin/blob/1d4846a8443be901b8a5deb0e357481af22838d0/src/wallet/spend.cpp#L936 + let mut cur_height = unlocked_state.channel_manager.current_best_block().height(); + // 10% of the time + if thread_rng().gen_range(0..10) == 0 { + // subtract random number between 0 and 100 + cur_height = cur_height.saturating_sub(thread_rng().gen_range(0..100)); + } + let lock_time: PackedLockTime = + LockTime::from_height(cur_height).map_or(PackedLockTime::ZERO, |l| l.into()); + let mut vanilla_output_descriptors = vec![]; let mut need_rgb_refresh = false; @@ -748,23 +771,36 @@ async fn _spend_outputs( let (tx, vout, consignment) = match outp { SpendableOutputDescriptor::StaticPaymentOutput(descriptor) => { - let signer = unlocked_state.keys_manager.derive_channel_keys( - descriptor.channel_value_satoshis, - &descriptor.channel_keys_id, - ); - let intermediate_wallet = - get_bdk_wallet_seckey(static_state.network, signer.payment_key); - sync_wallet(&intermediate_wallet, static_state.electrum_url.clone()); - let mut builder = intermediate_wallet.build_tx(); - builder - .add_utxos(&rgb_inputs) - .expect("valid utxos") - .fee_rate(FeeRate::from_sat_per_vb(FEE_RATE)) - .manually_selected_only() - .ordering(bdk::wallet::tx_builder::TxOrdering::Untouched) - .add_data(&[1]) - .drain_to(bdk_script); - let psbt = builder.finish().expect("valid psbt finish").0; + let input = vec![TxIn { + previous_output: descriptor.outpoint.into_bitcoin_outpoint(), + script_sig: Script::new(), + sequence: Sequence::from_consensus(1), + witness: Witness::new(), + }]; + let witness_weight = descriptor.max_witness_length(); + let input_value = descriptor.output.value; + let output = vec![TxOut { + value: 0, + script_pubkey: Script::new_op_return(&[1]), + }]; + let mut spend_tx = Transaction { + version: 2, + lock_time, + input, + output, + }; + let _expected_max_weight = + lightning::util::transaction_utils::maybe_add_change_output( + &mut spend_tx, + input_value, + witness_weight, + tx_feerate, + bdk_script, + ) + .expect("can add change"); + + let psbt = PartiallySignedTransaction::from_unsigned_tx(spend_tx.clone()) + .expect("valid transaction"); let (vout, asset_transition_builder) = update_transition_beneficiary( &psbt, @@ -773,20 +809,26 @@ async fn _spend_outputs( assignment_id, amt_rgb, ); - let (mut psbt, consignment) = + let (psbt, consignment) = runtime.send_rgb(contract_id, psbt, asset_transition_builder, beneficiaries); - intermediate_wallet - .sign(&mut psbt, SignOptions::default()) - .expect("able to sign"); - - (psbt.extract_tx(), vout, consignment) - } - SpendableOutputDescriptor::DelayedPaymentOutput(descriptor) => { + let input_idx = 0; + let mut spend_tx = psbt.extract_tx(); let signer = unlocked_state.keys_manager.derive_channel_keys( descriptor.channel_value_satoshis, &descriptor.channel_keys_id, ); + let witness_vec = signer + .sign_counterparty_payment_input( + &spend_tx, input_idx, descriptor, &secp_ctx, true, + ) + .expect("possible counterparty payment sign"); + + spend_tx.input[input_idx].witness = Witness::from_vec(witness_vec); + + (spend_tx, vout, consignment) + } + SpendableOutputDescriptor::DelayedPaymentOutput(descriptor) => { let input = vec![TxIn { previous_output: descriptor.outpoint.into_bitcoin_outpoint(), script_sig: Script::new(), @@ -801,7 +843,7 @@ async fn _spend_outputs( }]; let mut spend_tx = Transaction { version: 2, - lock_time: PackedLockTime(0), + lock_time, input, output, }; @@ -828,8 +870,12 @@ async fn _spend_outputs( let (psbt, consignment) = runtime.send_rgb(contract_id, psbt, asset_transition_builder, beneficiaries); - let mut spend_tx = psbt.extract_tx(); let input_idx = 0; + let mut spend_tx = psbt.extract_tx(); + let signer = unlocked_state.keys_manager.derive_channel_keys( + descriptor.channel_value_satoshis, + &descriptor.channel_keys_id, + ); let witness_vec = signer .sign_dynamic_p2wsh_input(&spend_tx, input_idx, descriptor, &secp_ctx) .expect("possible dynamic sign"); @@ -917,29 +963,12 @@ async fn _spend_outputs( let script_buf = address.script_pubkey(); let bdk_script = BdkScript::from(script_buf.into_bytes()); - // We set nLockTime to the current height to discourage fee sniping. - // Occasionally randomly pick a nLockTime even further back, so - // that transactions that are delayed after signing for whatever reason, - // e.g. high-latency mix networks and some CoinJoin implementations, have - // better privacy. - // Logic copied from core: https://github.com/bitcoin/bitcoin/blob/1d4846a8443be901b8a5deb0e357481af22838d0/src/wallet/spend.cpp#L936 - let mut cur_height = unlocked_state.channel_manager.current_best_block().height(); - - // 10% of the time - if thread_rng().gen_range(0..10) == 0 { - // subtract random number between 0 and 100 - cur_height = cur_height.saturating_sub(thread_rng().gen_range(0..100)); - } - - let locktime: PackedLockTime = - LockTime::from_height(cur_height).map_or(PackedLockTime::ZERO, |l| l.into()); - if let Ok(spending_tx) = unlocked_state.keys_manager.spend_spendable_outputs( output_descriptors, Vec::new(), bdk_script, tx_feerate, - Some(locktime), + Some(lock_time), &Secp256k1::new(), ) { // Note that, most likely, we've already sweeped this set of outputs @@ -1194,10 +1223,9 @@ pub(crate) async fn start_ldk( user_config .channel_handshake_limits .force_announced_channel_preference = false; - // TODO: set to true after implementing BumpTxEventHandler user_config .channel_handshake_config - .negotiate_anchors_zero_fee_htlc_tx = false; + .negotiate_anchors_zero_fee_htlc_tx = true; user_config.manually_accept_inbound_channels = true; let mut restarting_node = true; let (channel_manager_blockhash, channel_manager) = { @@ -1456,6 +1484,19 @@ pub(crate) async fn start_ldk( ) .expect("able to write"); + let rgb_wallet = Arc::new(Mutex::new(rgb_wallet)); + let rgb_wallet_wrapper = Arc::new(RgbLibWalletWrapper::new( + Arc::clone(&rgb_wallet), + rgb_online.clone(), + )); + + let bump_tx_event_handler = Arc::new(BumpTransactionEventHandler::new( + Arc::clone(&broadcaster), + Arc::new(Wallet::new(rgb_wallet_wrapper, Arc::clone(&logger))), + Arc::clone(&keys_manager), + Arc::clone(&logger), + )); + // Persist ChannelManager and NetworkGraph let persister = Arc::new(FilesystemStore::new(ldk_data_dir_path.clone())); @@ -1469,7 +1510,8 @@ pub(crate) async fn start_ldk( peer_manager: Arc::clone(&peer_manager), fs_store: Arc::clone(&fs_store), persister: Arc::clone(&persister), - rgb_wallet: Arc::new(Mutex::new(rgb_wallet)), + bump_tx_event_handler, + rgb_wallet, rgb_online, }); diff --git a/src/rgb.rs b/src/rgb.rs index d84dd6d..64af486 100644 --- a/src/rgb.rs +++ b/src/rgb.rs @@ -1,15 +1,24 @@ use amplify::ByteArray; use bdk::bitcoin::psbt::PartiallySignedTransaction; -use bitcoin::Network; -use bitcoin_30::hashes::Hash; +use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; +use bitcoin::hashes::hex::{FromHex, ToHex}; +use bitcoin::hashes::Hash; +use bitcoin::psbt::Psbt; +use bitcoin::util::address::{Payload, WitnessVersion}; +use bitcoin::{ + Address, Network, OutPoint, Script, Transaction, TxOut, WPubkeyHash, XOnlyPublicKey, +}; +use bitcoin_30::hashes::Hash as Hash30; use bitcoin_30::psbt::PartiallySignedTransaction as RgbPsbt; use bp::seals::txout::blind::{BlindSeal, SingleBlindSeal}; use bp::seals::txout::{CloseMethod, TxPtr}; use bp::Outpoint as RgbOutpoint; +use lightning::events::bump_transaction::{Utxo, WalletSource}; use lightning::rgb_utils::STATIC_BLINDING; use rgb_core::Operation; use rgb_lib::utils::RgbRuntime; -use rgb_lib::BitcoinNetwork; +use rgb_lib::wallet::Online; +use rgb_lib::{BitcoinNetwork, SignOptions, Wallet as RgbLibWallet}; use rgbstd::containers::{Bindle, BuilderSeal, Transfer as RgbTransfer}; use rgbstd::contract::{ContractId, GraphSeal}; use rgbstd::interface::{TransitionBuilder, TypedState}; @@ -19,6 +28,7 @@ use rgbwallet::psbt::opret::OutputOpret; use rgbwallet::psbt::{PsbtDbc, RgbExt, RgbInExt}; use std::collections::HashMap; use std::str::FromStr; +use std::sync::{Arc, Mutex}; use crate::error::APIError; @@ -188,3 +198,78 @@ impl RgbUtilities for RgbRuntime { (psbt, transfer) } } + +pub(crate) struct RgbLibWalletWrapper { + pub(crate) wallet: Arc>, + pub(crate) online: Online, +} + +impl RgbLibWalletWrapper { + pub(crate) fn new(wallet: Arc>, online: Online) -> Self { + RgbLibWalletWrapper { wallet, online } + } +} + +impl WalletSource for RgbLibWalletWrapper { + fn list_confirmed_utxos(&self) -> Result, ()> { + let wallet = self.wallet.lock().unwrap(); + let network = Network::from_str( + &wallet + .get_wallet_data() + .bitcoin_network + .to_string() + .to_lowercase(), + ) + .unwrap(); + Ok(wallet.list_unspents_vanilla(self.online.clone(), 1).unwrap().iter().filter_map(|u| { + let script = Script::from_hex(&u.txout.script_pubkey.to_hex()).unwrap(); + let address = Address::from_script(&script, network).unwrap(); + let outpoint = OutPoint::from_str(&u.outpoint.to_string()).unwrap(); + match address.payload { + Payload::WitnessProgram { version, ref program } => match version { + WitnessVersion::V0 => WPubkeyHash::from_slice(program) + .map(|wpkh| Utxo::new_v0_p2wpkh(outpoint, u.txout.value, &wpkh)) + .ok(), + // TODO: Add `Utxo::new_v1_p2tr` upstream. + WitnessVersion::V1 => XOnlyPublicKey::from_slice(program) + .map(|_| Utxo { + outpoint, + output: TxOut { + value: u.txout.value, + script_pubkey: Script::new_witness_program(version, program), + }, + satisfaction_weight: WITNESS_SCALE_FACTOR as u64 + + 1 /* witness items */ + 1 /* schnorr sig len */ + 64, /* schnorr sig */ + }) + .ok(), + _ => None, + }, + _ => None, + } + }) + .collect()) + } + + fn get_change_script(&self) -> Result { + Ok( + Address::from_str(&self.wallet.lock().unwrap().get_address().unwrap()) + .unwrap() + .script_pubkey(), + ) + } + + fn sign_tx(&self, tx: Transaction) -> Result { + let psbt = RgbPsbt::from_str(&Psbt::from_unsigned_tx(tx).unwrap().to_string()).unwrap(); + let sign_options = SignOptions { + trust_witness_utxo: true, + ..Default::default() + }; + let signed = self + .wallet + .lock() + .unwrap() + .sign_psbt(psbt.to_string(), Some(sign_options)) + .unwrap(); + Ok(Psbt::from_str(&signed).unwrap().extract_tx()) + } +} diff --git a/src/routes.rs b/src/routes.rs index b7f88bf..0fa79b1 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -390,6 +390,7 @@ pub(crate) struct OpenChannelRequest { pub(crate) asset_amount: u64, pub(crate) asset_id: String, pub(crate) public: bool, + pub(crate) with_anchors: bool, } #[derive(Deserialize, Serialize)] @@ -1469,6 +1470,10 @@ pub(crate) async fn open_channel( ))); } + if !payload.with_anchors { + return Err(APIError::AnchorsRequired); + } + connect_peer_if_necessary(peer_pubkey, peer_addr, unlocked_state.peer_manager.clone()) .await?; @@ -1493,8 +1498,7 @@ pub(crate) async fn open_channel( announced_channel: payload.public, our_htlc_minimum_msat: HTLC_MIN_MSAT, minimum_depth: MIN_CHANNEL_CONFIRMATIONS as u32, - // TODO: set to true after implementing BumpTxEventHandler - negotiate_anchors_zero_fee_htlc_tx: false, + negotiate_anchors_zero_fee_htlc_tx: payload.with_anchors, ..Default::default() }, ..Default::default() diff --git a/src/test/mod.rs b/src/test/mod.rs index ccf0987..2b19c99 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -500,6 +500,7 @@ async fn open_channel( asset_amount, asset_id: asset_id.to_string(), public: true, + with_anchors: true, }; let res = reqwest::Client::new() .post(format!("http://{}/openchannel", node_address)) diff --git a/src/utils.rs b/src/utils.rs index cabd07c..4898698 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -31,8 +31,8 @@ use crate::{ disk::FilesystemLogger, error::{APIError, AppError}, ldk::{ - ChannelManager, InboundPaymentInfoStorage, LdkBackgroundServices, NetworkGraph, - OnionMessenger, OutboundPaymentInfoStorage, PeerManager, + BumpTxEventHandler, ChannelManager, InboundPaymentInfoStorage, LdkBackgroundServices, + NetworkGraph, OnionMessenger, OutboundPaymentInfoStorage, PeerManager, }, rgb::get_bitcoin_network, }; @@ -94,6 +94,7 @@ pub(crate) struct UnlockedAppState { pub(crate) peer_manager: Arc, pub(crate) fs_store: Arc, pub(crate) persister: Arc, + pub(crate) bump_tx_event_handler: Arc, pub(crate) rgb_wallet: Arc>, pub(crate) rgb_online: Online, }