Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,17 @@ default = []
#lightning-liquidity = { version = "0.2.0", features = ["std"] }
#lightning-macros = { version = "0.2.0" }

lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["std"] }
lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" }
lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["std"] }
lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" }
lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["tokio"] }
lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" }
lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" }
lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["rest-client", "rpc-client", "tokio"] }
lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["esplora-async-https", "time", "electrum-rustls-ring"] }
lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["std"] }
lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0" }
lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d", features = ["std"] }
lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d" }
lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d", features = ["std"] }
lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d" }
lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d", features = ["tokio"] }
lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d" }
lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d" }
lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d", features = ["rest-client", "rpc-client", "tokio"] }
lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d", features = ["esplora-async-https", "time", "electrum-rustls-ring"] }
lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d", features = ["std"] }
lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d" }

bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] }
bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]}
Expand Down Expand Up @@ -78,13 +78,13 @@ log = { version = "0.4.22", default-features = false, features = ["std"]}
vss-client = { package = "vss-client-ng", version = "0.4" }
prost = { version = "0.11.6", default-features = false}
#bitcoin-payment-instructions = { version = "0.6" }
bitcoin-payment-instructions = { git = "https://github.com/tnull/bitcoin-payment-instructions", rev = "ea50a9d2a8da524b69a2af43233706666cf2ffa5" }
bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-payment-instructions", rev = "c1a4b49d0406ff5cff0267dc9b5e64a2bae95929" }

[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winbase"] }

[dev-dependencies]
lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b6c17c593a5d7bacb18fe3b9f69074a0596ae8f0", features = ["std", "_test_utils"] }
lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "0a20fa5340b5788c2472febbfbb631506b15c42d", features = ["std", "_test_utils"] }
proptest = "1.0.0"
regex = "1.5.6"
criterion = { version = "0.7.0", features = ["async_tokio"] }
Expand Down
120 changes: 38 additions & 82 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,10 @@ use gossip::GossipSource;
use graph::NetworkGraph;
use io::utils::write_node_metrics;
use lightning::chain::BestBlock;
use lightning::events::bump_transaction::{Input, Wallet as LdkWallet};
use lightning::events::bump_transaction::Wallet as LdkWallet;
use lightning::impl_writeable_tlv_based;
use lightning::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT;
use lightning::ln::channel_state::{ChannelDetails as LdkChannelDetails, ChannelShutdownState};
use lightning::ln::channelmanager::PaymentId;
use lightning::ln::funding::SpliceContribution;
use lightning::ln::msgs::SocketAddress;
use lightning::routing::gossip::NodeAlias;
use lightning::util::persist::KVStoreSync;
Expand Down Expand Up @@ -1290,84 +1288,37 @@ impl Node {
{
self.check_sufficient_funds_for_channel(splice_amount_sats, &counterparty_node_id)?;

const EMPTY_SCRIPT_SIG_WEIGHT: u64 =
1 /* empty script_sig */ * bitcoin::constants::WITNESS_SCALE_FACTOR as u64;

let funding_txo = channel_details.funding_txo.ok_or_else(|| {
log_error!(self.logger, "Failed to splice channel: channel not yet ready",);
Error::ChannelSplicingFailed
})?;

let funding_output = channel_details.get_funding_output().ok_or_else(|| {
log_error!(self.logger, "Failed to splice channel: channel not yet ready");
Error::ChannelSplicingFailed
})?;

let shared_input = Input {
outpoint: funding_txo.into_bitcoin_outpoint(),
previous_utxo: funding_output.clone(),
satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT,
};

let shared_output = bitcoin::TxOut {
value: shared_input.previous_utxo.value + Amount::from_sat(splice_amount_sats),
// will not actually be the exact same script pubkey after splice
// but it is the same size and good enough for coin selection purposes
script_pubkey: funding_output.script_pubkey.clone(),
};

let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);

let inputs = self
.wallet
.select_confirmed_utxos(vec![shared_input], &[shared_output], fee_rate)
.map_err(|()| {
log_error!(
self.logger,
"Failed to splice channel: insufficient confirmed UTXOs",
);
let funding_template = self
.channel_manager
.splice_channel(&channel_details.channel_id, &counterparty_node_id, fee_rate)
.map_err(|e| {
log_error!(self.logger, "Failed to splice channel: {:?}", e);
Error::ChannelSplicingFailed
})?;

let change_address = self.wallet.get_new_internal_address()?;

let contribution = SpliceContribution::splice_in(
Amount::from_sat(splice_amount_sats),
inputs,
Some(change_address.script_pubkey()),
);

let funding_feerate_per_kw: u32 = match fee_rate.to_sat_per_kwu().try_into() {
Ok(fee_rate) => fee_rate,
Err(_) => {
debug_assert!(false);
fee_estimator::get_fallback_rate_for_target(ConfirmationTarget::ChannelFunding)
},
};
let contribution = self
.runtime
.block_on(
funding_template
.splice_in(Amount::from_sat(splice_amount_sats), Arc::clone(&self.wallet)),
)
.map_err(|()| {
log_error!(self.logger, "Failed to splice channel: coin selection failed");
Error::ChannelSplicingFailed
})?;

self.channel_manager
.splice_channel(
.funding_contributed(
&channel_details.channel_id,
&counterparty_node_id,
contribution,
funding_feerate_per_kw,
None,
)
.map_err(|e| {
log_error!(self.logger, "Failed to splice channel: {:?}", e);
let tx = bitcoin::Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: bitcoin::absolute::LockTime::ZERO,
input: vec![],
output: vec![bitcoin::TxOut {
value: Amount::ZERO,
script_pubkey: change_address.script_pubkey(),
}],
};
match self.wallet.cancel_tx(&tx) {
Ok(()) => Error::ChannelSplicingFailed,
Err(e) => e,
}
Error::ChannelSplicingFailed
})
} else {
log_error!(
Expand All @@ -1376,7 +1327,6 @@ impl Node {
user_channel_id,
counterparty_node_id
);

Err(Error::ChannelSplicingFailed)
}
}
Expand Down Expand Up @@ -1407,27 +1357,33 @@ impl Node {

self.wallet.parse_and_validate_address(address)?;

let contribution = SpliceContribution::splice_out(vec![bitcoin::TxOut {
let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);

let funding_template = self
.channel_manager
.splice_channel(&channel_details.channel_id, &counterparty_node_id, fee_rate)
.map_err(|e| {
log_error!(self.logger, "Failed to splice channel: {:?}", e);
Error::ChannelSplicingFailed
})?;

let outputs = vec![bitcoin::TxOut {
value: Amount::from_sat(splice_amount_sats),
script_pubkey: address.script_pubkey(),
}]);

let fee_rate = self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding);
let funding_feerate_per_kw: u32 = match fee_rate.to_sat_per_kwu().try_into() {
Ok(fee_rate) => fee_rate,
Err(_) => {
debug_assert!(false, "FeeRate should always fit within u32");
log_error!(self.logger, "FeeRate should always fit within u32");
fee_estimator::get_fallback_rate_for_target(ConfirmationTarget::ChannelFunding)
},
};
}];
let contribution = self
.runtime
.block_on(funding_template.splice_out(outputs, Arc::clone(&self.wallet)))
.map_err(|()| {
log_error!(self.logger, "Failed to splice channel: coin selection failed");
Error::ChannelSplicingFailed
})?;

self.channel_manager
.splice_channel(
.funding_contributed(
&channel_details.channel_id,
&counterparty_node_id,
contribution,
funding_feerate_per_kw,
None,
)
.map_err(|e| {
Expand Down
2 changes: 2 additions & 0 deletions src/payment/unified.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ impl UnifiedPayment {
PaymentMethod::LightningBolt12(_) => 0,
PaymentMethod::LightningBolt11(_) => 1,
PaymentMethod::OnChain(_) => 2,
PaymentMethod::Cashu(_) => 3,
});

for method in sorted_payment_methods {
Expand Down Expand Up @@ -278,6 +279,7 @@ impl UnifiedPayment {
let txid = self.onchain_payment.send_to_address(&address, amt_sats, None)?;
return Ok(UnifiedPaymentResult::Onchain { txid });
},
PaymentMethod::Cashu(_) => todo!(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh, let's not panic if someone includes Cashu instructions in the string above. Could just error and skip if this is hit.

}
}

Expand Down
80 changes: 74 additions & 6 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ use bitcoin::psbt::{self, Psbt};
use bitcoin::secp256k1::ecdh::SharedSecret;
use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey};
use bitcoin::transaction::Sequence;
use bitcoin::{
Address, Amount, FeeRate, OutPoint, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, Weight,
WitnessProgram, WitnessVersion,
};
use lightning::chain::chaininterface::BroadcasterInterface;
use lightning::chain::channelmonitor::ANTI_REORG_DELAY;
use lightning::chain::{BestBlock, Listen};
use lightning::events::bump_transaction::{Input, Utxo, WalletSource};
use lightning::chain::{BestBlock, ClaimId, Listen};
use lightning::events::bump_transaction::{
CoinSelection, CoinSelectionSource, Input, Utxo, WalletSource,
};
use lightning::ln::channelmanager::PaymentId;
use lightning::ln::funding::FundingTxInput;
use lightning::ln::inbound_payment::ExpandedKey;
Expand Down Expand Up @@ -710,8 +713,10 @@ impl Wallet {

pub(crate) fn select_confirmed_utxos(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tnull I'm wondering if we should implement CoinSelectionSource and use this rather than using LdkWallet for UTXO selection. It would prevent double persistence when we need a change output and the need for an explicit persist method after selection.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that seems preferable, if we have everything we need in the API by now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I think the question may be whether we use our own CoinSelectionSource for anchor bumping, too. It depends on how UTXO locking will work. LdkWallet manages whether or not to try to double-spend based on the claim id, whereas for splicing we may accidentally double spend one of those if using our own CoinSelectionSource. Pushed a fixup commit implementing CoinSelectionSource for splicing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, well the branch shows the fixup. Not sure why the PR hasn't updated.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I think the question may be whether we use our own CoinSelectionSource for anchor bumping, too.

It would be great to reuse the same codepaths, IMO. That is, if the API now permits for it.

It depends on how UTXO locking will work. LdkWallet manages whether or not to try to double-spend based on the claim id, whereas for splicing we may accidentally double spend one of those if using our own CoinSelectionSource.

Right, we're still waiting on bitcoindevkit/bdk_wallet#259 to ship with BDK 3.0. For now I'm not sure if we'd then need to also do some tracking based on ClaimId.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline. We'll use the current approach and then once BDK ships a new release we can revisit coin selection for anchor bumping.

Alternatives would be to either (1) drop the fixup such that we use LdkWallet for both coin selections or (2) maintain a locked UTXO list for splice coin selection and filter WalletSource::list_confirmed_utxos so that anchor bumping doesn't double spend splicing.

&self, must_spend: Vec<Input>, must_pay_to: &[TxOut], fee_rate: FeeRate,
) -> Result<Vec<FundingTxInput>, ()> {
) -> Result<CoinSelection, ()> {
let mut locked_wallet = self.inner.lock().unwrap();
let mut locked_persister = self.persister.lock().unwrap();

debug_assert!(matches!(
locked_wallet.public_descriptor(KeychainKind::External),
ExtendedDescriptor::Wpkh(_)
Expand Down Expand Up @@ -740,12 +745,14 @@ impl Wallet {
tx_builder.fee_rate(fee_rate);
tx_builder.exclude_unconfirmed();

tx_builder
let unsigned_tx = tx_builder
.finish()
.map_err(|e| {
log_error!(self.logger, "Failed to select confirmed UTXOs: {}", e);
})?
.unsigned_tx
.unsigned_tx;

let confirmed_utxos = unsigned_tx
.input
.iter()
.filter(|txin| must_spend.iter().all(|input| input.outpoint != txin.previous_output))
Expand All @@ -755,7 +762,29 @@ impl Wallet {
.map(|tx_details| tx_details.tx.deref().clone())
.map(|prevtx| FundingTxInput::new_p2wpkh(prevtx, txin.previous_output.vout))
})
.collect::<Result<Vec<_>, ()>>()
.collect::<Result<Vec<_>, ()>>()?;

if unsigned_tx.output.len() > must_pay_to.len() + 1 {
log_error!(
self.logger,
"Unexpected number of change outputs during coin selection: {}",
unsigned_tx.output.len() - must_pay_to.len(),
);
return Err(());
}

let change_output = unsigned_tx
.output
.into_iter()
.filter(|txout| must_pay_to.iter().all(|output| output != txout))
.next();

locked_wallet.persist(&mut locked_persister).map_err(|e| {
log_error!(self.logger, "Failed to persist wallet: {}", e);
()
})?;

Ok(CoinSelection { confirmed_utxos, change_output })
}

fn list_confirmed_utxos_inner(&self) -> Result<Vec<Utxo>, ()> {
Expand Down Expand Up @@ -831,6 +860,7 @@ impl Wallet {
},
satisfaction_weight: 1 /* empty script_sig */ * WITNESS_SCALE_FACTOR as u64 +
1 /* witness items */ + 1 /* schnorr sig len */ + 64, // schnorr sig
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
};
utxos.push(utxo);
},
Expand Down Expand Up @@ -1094,9 +1124,47 @@ impl WalletSource for Wallet {
async move { self.get_change_script_inner() }
}

fn get_prevtx<'a>(
&'a self, outpoint: OutPoint,
) -> impl Future<Output = Result<Transaction, ()>> + Send + 'a {
async move {
let locked_wallet = self.inner.lock().unwrap();
locked_wallet
.tx_details(outpoint.txid)
.map(|tx_details| tx_details.tx.deref().clone())
.ok_or_else(|| {
log_error!(
self.logger,
"Failed to get previous transaction for {}",
outpoint.txid
);
})
}
}

fn sign_psbt<'a>(
&'a self, psbt: Psbt,
) -> impl Future<Output = Result<Transaction, ()>> + Send + 'a {
async move { self.sign_psbt_inner(psbt) }
}
}

// Anchor bumping uses LdkWallet for coin selection, which wraps a WalletSource to implement
// CoinSelectionSource. Splicing uses this implementation of coin selection instead.
impl CoinSelectionSource for Wallet {
fn select_confirmed_utxos<'a>(
&'a self, claim_id: Option<ClaimId>, must_spend: Vec<Input>, must_pay_to: &'a [TxOut],
target_feerate_sat_per_1000_weight: u32, _max_tx_weight: u64,
) -> impl Future<Output = Result<CoinSelection, ()>> + Send + 'a {
debug_assert!(claim_id.is_none());
let fee_rate = FeeRate::from_sat_per_kwu(target_feerate_sat_per_1000_weight as u64);
async move { self.select_confirmed_utxos(must_spend, must_pay_to, fee_rate) }
}

fn sign_psbt<'a>(
&'a self, psbt: Psbt,
) -> impl Future<Output = Result<Transaction, ()>> + Send + 'a {
debug_assert!(false);
async move { self.sign_psbt_inner(psbt) }
}
}
Expand Down
2 changes: 1 addition & 1 deletion tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -983,7 +983,7 @@ async fn splice_channel() {
expect_channel_ready_event!(node_a, node_b.node_id());
expect_channel_ready_event!(node_b, node_a.node_id());

let expected_splice_in_fee_sat = 252;
let expected_splice_in_fee_sat = 255;

let payments = node_b.list_payments();
let payment =
Expand Down
Loading