mirror of
https://github.com/lightningdevkit/rust-lightning.git
synced 2025-03-13 14:52:21 +01:00
Implement initial splice negotiation
Handle the initial splice negotiation, without continuing to transaction negotiation: - `splice_channel()` for initiating splicing - handling of `splice_init` and `splice_ack` messages, with checks - `splice_ack` always fails (use case still imcomplete) - A test to go through the use case (`test_v1_splice_in`)
This commit is contained in:
parent
c4d0560c1e
commit
dc3ab7b382
8 changed files with 700 additions and 20 deletions
|
@ -81,6 +81,27 @@ pub const HTLC_TIMEOUT_INPUT_ANCHOR_WITNESS_WEIGHT: u64 = 288;
|
|||
/// outputs.
|
||||
pub const HTLC_SUCCESS_INPUT_ANCHOR_WITNESS_WEIGHT: u64 = 327;
|
||||
|
||||
/// The size of the 2-of-2 multisig script
|
||||
const MULTISIG_SCRIPT_SIZE: u64 =
|
||||
1 + // OP_2
|
||||
1 + // data len
|
||||
33 + // pubkey1
|
||||
1 + // data len
|
||||
33 + // pubkey2
|
||||
1 + // OP_2
|
||||
1; // OP_CHECKMULTISIG
|
||||
/// The weight of a funding transaction input (2-of-2 P2WSH)
|
||||
/// See https://github.com/lightning/bolts/blob/master/03-transactions.md#expected-weight-of-the-commitment-transaction
|
||||
pub const FUNDING_TRANSACTION_WITNESS_WEIGHT: u64 =
|
||||
1 + // number_of_witness_elements
|
||||
1 + // nil_len
|
||||
1 + // sig len
|
||||
73 + // sig1
|
||||
1 + // sig len
|
||||
73 + // sig2
|
||||
1 + // witness_script_length
|
||||
MULTISIG_SCRIPT_SIZE;
|
||||
|
||||
/// Gets the weight for an HTLC-Success transaction.
|
||||
#[inline]
|
||||
pub fn htlc_success_tx_weight(channel_type_features: &ChannelTypeFeatures) -> u64 {
|
||||
|
|
|
@ -11,7 +11,6 @@ use bitcoin::amount::Amount;
|
|||
use bitcoin::constants::ChainHash;
|
||||
use bitcoin::script::{Script, ScriptBuf, Builder, WScriptHash};
|
||||
use bitcoin::transaction::{Transaction, TxIn};
|
||||
use bitcoin::sighash;
|
||||
use bitcoin::sighash::EcdsaSighashType;
|
||||
use bitcoin::consensus::encode;
|
||||
use bitcoin::absolute::LockTime;
|
||||
|
@ -25,7 +24,7 @@ use bitcoin::hash_types::{Txid, BlockHash};
|
|||
use bitcoin::secp256k1::constants::PUBLIC_KEY_SIZE;
|
||||
use bitcoin::secp256k1::{PublicKey,SecretKey};
|
||||
use bitcoin::secp256k1::{Secp256k1,ecdsa::Signature};
|
||||
use bitcoin::secp256k1;
|
||||
use bitcoin::{secp256k1, sighash};
|
||||
|
||||
use crate::ln::types::ChannelId;
|
||||
use crate::types::payment::{PaymentPreimage, PaymentHash};
|
||||
|
@ -48,6 +47,8 @@ use crate::ln::chan_utils::{
|
|||
get_commitment_transaction_number_obscure_factor,
|
||||
ClosingTransaction, commit_tx_fee_sat,
|
||||
};
|
||||
#[cfg(splicing)]
|
||||
use crate::ln::chan_utils::FUNDING_TRANSACTION_WITNESS_WEIGHT;
|
||||
use crate::ln::chan_utils;
|
||||
use crate::ln::onion_utils::HTLCFailReason;
|
||||
use crate::chain::BestBlock;
|
||||
|
@ -1517,6 +1518,8 @@ impl<SP: Deref> Channel<SP> where
|
|||
interactive_tx_signing_session: chan.interactive_tx_signing_session,
|
||||
holder_commitment_point,
|
||||
is_v2_established: true,
|
||||
#[cfg(splicing)]
|
||||
pending_splice: None,
|
||||
};
|
||||
let res = funded_channel.commitment_signed_initial_v2(msg, best_block, signer_provider, logger)
|
||||
.map(|monitor| (Some(monitor), None))
|
||||
|
@ -1719,6 +1722,12 @@ impl FundingScope {
|
|||
}
|
||||
}
|
||||
|
||||
/// Info about a pending splice, used in the pre-splice channel
|
||||
#[cfg(splicing)]
|
||||
struct PendingSplice {
|
||||
pub our_funding_contribution: i64,
|
||||
}
|
||||
|
||||
/// Contains everything about the channel including state, and various flags.
|
||||
pub(super) struct ChannelContext<SP: Deref> where SP::Target: SignerProvider {
|
||||
config: LegacyChannelConfig,
|
||||
|
@ -4797,6 +4806,60 @@ fn estimate_v2_funding_transaction_fee(
|
|||
fee_for_weight(funding_feerate_sat_per_1000_weight, weight)
|
||||
}
|
||||
|
||||
/// Verify that the provided inputs to the funding transaction are enough
|
||||
/// to cover the intended contribution amount *plus* the proportional fees.
|
||||
/// Fees are computed using `estimate_v2_funding_transaction_fee`, and contain
|
||||
/// the fees of the inputs, fees of the inputs weight, and for the initiator,
|
||||
/// the fees of the common fields as well as the output and extra input weights.
|
||||
/// Returns estimated (partial) fees as additional information
|
||||
#[cfg(splicing)]
|
||||
fn check_v2_funding_inputs_sufficient(
|
||||
contribution_amount: i64, funding_inputs: &[(TxIn, Transaction, Weight)], is_initiator: bool,
|
||||
is_splice: bool, funding_feerate_sat_per_1000_weight: u32,
|
||||
) -> Result<u64, ChannelError> {
|
||||
let mut total_input_witness_weight = Weight::from_wu(funding_inputs.iter().map(|(_, _, w)| w.to_wu()).sum());
|
||||
let mut funding_inputs_len = funding_inputs.len();
|
||||
if is_initiator && is_splice {
|
||||
// consider the weight of the input and witness needed for spending the old funding transaction
|
||||
funding_inputs_len += 1;
|
||||
total_input_witness_weight += Weight::from_wu(FUNDING_TRANSACTION_WITNESS_WEIGHT);
|
||||
}
|
||||
let estimated_fee = estimate_v2_funding_transaction_fee(is_initiator, funding_inputs_len, total_input_witness_weight, funding_feerate_sat_per_1000_weight);
|
||||
|
||||
let mut total_input_sats = 0u64;
|
||||
for (idx, input) in funding_inputs.iter().enumerate() {
|
||||
if let Some(output) = input.1.output.get(input.0.previous_output.vout as usize) {
|
||||
total_input_sats = total_input_sats.saturating_add(output.value.to_sat());
|
||||
} else {
|
||||
return Err(ChannelError::Warn(format!(
|
||||
"Transaction with txid {} does not have an output with vout of {} corresponding to TxIn at funding_inputs[{}]",
|
||||
input.1.compute_txid(), input.0.previous_output.vout, idx
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// If the inputs are enough to cover intended contribution amount, with fees even when
|
||||
// there is a change output, we are fine.
|
||||
// If the inputs are less, but enough to cover intended contribution amount, with
|
||||
// (lower) fees with no change, we are also fine (change will not be generated).
|
||||
// So it's enough to check considering the lower, no-change fees.
|
||||
//
|
||||
// Note: dust limit is not relevant in this check.
|
||||
//
|
||||
// TODO(splicing): refine check including the fact wether a change will be added or not.
|
||||
// Can be done once dual funding preparation is included.
|
||||
|
||||
let minimal_input_amount_needed = contribution_amount.saturating_add(estimated_fee as i64);
|
||||
if (total_input_sats as i64) < minimal_input_amount_needed {
|
||||
Err(ChannelError::Warn(format!(
|
||||
"Total input amount {} is lower than needed for contribution {}, considering fees of {}. Need more inputs.",
|
||||
total_input_sats, contribution_amount, estimated_fee,
|
||||
)))
|
||||
} else {
|
||||
Ok(estimated_fee)
|
||||
}
|
||||
}
|
||||
|
||||
/// Context for dual-funded channels.
|
||||
pub(super) struct DualFundingChannelContext {
|
||||
/// The amount in satoshis we will be contributing to the channel.
|
||||
|
@ -4826,6 +4889,9 @@ pub(super) struct FundedChannel<SP: Deref> where SP::Target: SignerProvider {
|
|||
/// Indicates whether this funded channel had been established with V2 channel
|
||||
/// establishment.
|
||||
is_v2_established: bool,
|
||||
/// Info about an in-progress, pending splice (if any), on the pre-splice channel
|
||||
#[cfg(splicing)]
|
||||
pending_splice: Option<PendingSplice>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, fuzzing))]
|
||||
|
@ -8360,6 +8426,141 @@ impl<SP: Deref> FundedChannel<SP> where
|
|||
}
|
||||
}
|
||||
|
||||
/// Initiate splicing.
|
||||
/// - `our_funding_inputs`: the inputs we contribute to the new funding transaction.
|
||||
/// Includes the witness weight for this input (e.g. P2WPKH_WITNESS_WEIGHT=109 for typical P2WPKH inputs).
|
||||
#[cfg(splicing)]
|
||||
pub fn splice_channel(&mut self, our_funding_contribution_satoshis: i64,
|
||||
our_funding_inputs: &Vec<(TxIn, Transaction, Weight)>,
|
||||
funding_feerate_per_kw: u32, locktime: u32,
|
||||
) -> Result<msgs::SpliceInit, APIError> {
|
||||
// Check if a splice has been initiated already.
|
||||
// Note: only a single outstanding splice is supported (per spec)
|
||||
if let Some(splice_info) = &self.pending_splice {
|
||||
return Err(APIError::APIMisuseError { err: format!(
|
||||
"Channel {} cannot be spliced, as it has already a splice pending (contribution {})",
|
||||
self.context.channel_id(), splice_info.our_funding_contribution
|
||||
)});
|
||||
}
|
||||
|
||||
if !self.context.is_live() {
|
||||
return Err(APIError::APIMisuseError { err: format!(
|
||||
"Channel {} cannot be spliced, as channel is not live",
|
||||
self.context.channel_id()
|
||||
)});
|
||||
}
|
||||
|
||||
// TODO(splicing): check for quiescence
|
||||
|
||||
if our_funding_contribution_satoshis < 0 {
|
||||
return Err(APIError::APIMisuseError { err: format!(
|
||||
"TODO(splicing): Splice-out not supported, only splice in; channel ID {}, contribution {}",
|
||||
self.context.channel_id(), our_funding_contribution_satoshis,
|
||||
)});
|
||||
}
|
||||
|
||||
// TODO(splicing): Once splice-out is supported, check that channel balance does not go below 0
|
||||
// (or below channel reserve)
|
||||
|
||||
// Note: post-splice channel value is not yet known at this point, counterparty contribution is not known
|
||||
// (Cannot test for miminum required post-splice channel value)
|
||||
|
||||
// Check that inputs are sufficient to cover our contribution.
|
||||
let _fee = check_v2_funding_inputs_sufficient(our_funding_contribution_satoshis, &our_funding_inputs, true, true, funding_feerate_per_kw)
|
||||
.map_err(|err| APIError::APIMisuseError { err: format!(
|
||||
"Insufficient inputs for splicing; channel ID {}, err {}",
|
||||
self.context.channel_id(), err,
|
||||
)})?;
|
||||
|
||||
self.pending_splice = Some(PendingSplice {
|
||||
our_funding_contribution: our_funding_contribution_satoshis,
|
||||
});
|
||||
|
||||
let msg = self.get_splice_init(our_funding_contribution_satoshis, funding_feerate_per_kw, locktime);
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Get the splice message that can be sent during splice initiation.
|
||||
#[cfg(splicing)]
|
||||
fn get_splice_init(&self, our_funding_contribution_satoshis: i64,
|
||||
funding_feerate_per_kw: u32, locktime: u32,
|
||||
) -> msgs::SpliceInit {
|
||||
// TODO(splicing): The exisiting pubkey is reused, but a new one should be generated. See #3542.
|
||||
// Note that channel_keys_id is supposed NOT to change
|
||||
let funding_pubkey = self.funding.get_holder_pubkeys().funding_pubkey.clone();
|
||||
msgs::SpliceInit {
|
||||
channel_id: self.context.channel_id,
|
||||
funding_contribution_satoshis: our_funding_contribution_satoshis,
|
||||
funding_feerate_per_kw,
|
||||
locktime,
|
||||
funding_pubkey,
|
||||
require_confirmed_inputs: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle splice_init
|
||||
#[cfg(splicing)]
|
||||
pub fn splice_init(&mut self, msg: &msgs::SpliceInit) -> Result<msgs::SpliceAck, ChannelError> {
|
||||
let their_funding_contribution_satoshis = msg.funding_contribution_satoshis;
|
||||
// TODO(splicing): Currently not possible to contribute on the splicing-acceptor side
|
||||
let our_funding_contribution_satoshis = 0i64;
|
||||
|
||||
// Check if a splice has been initiated already.
|
||||
if let Some(splice_info) = &self.pending_splice {
|
||||
return Err(ChannelError::Warn(format!(
|
||||
"Channel has already a splice pending, contribution {}", splice_info.our_funding_contribution,
|
||||
)));
|
||||
}
|
||||
|
||||
// - If it has received shutdown:
|
||||
// MUST send a warning and close the connection or send an error
|
||||
// and fail the channel.
|
||||
if !self.context.is_live() {
|
||||
return Err(ChannelError::Warn(format!("Splicing requested on a channel that is not live")));
|
||||
}
|
||||
|
||||
if their_funding_contribution_satoshis.saturating_add(our_funding_contribution_satoshis) < 0 {
|
||||
return Err(ChannelError::Warn(format!(
|
||||
"Splice-out not supported, only splice in, contribution is {} ({} + {})",
|
||||
their_funding_contribution_satoshis + our_funding_contribution_satoshis,
|
||||
their_funding_contribution_satoshis, our_funding_contribution_satoshis,
|
||||
)));
|
||||
}
|
||||
|
||||
// TODO(splicing): Once splice acceptor can contribute, check that inputs are sufficient,
|
||||
// similarly to the check in `splice_channel`.
|
||||
|
||||
// Note on channel reserve requirement pre-check: as the splice acceptor does not contribute,
|
||||
// it can't go below reserve, therefore no pre-check is done here.
|
||||
// TODO(splicing): Once splice acceptor can contribute, add reserve pre-check, similar to the one in `splice_ack`.
|
||||
|
||||
// TODO(splicing): Store msg.funding_pubkey
|
||||
// TODO(splicing): Apply start of splice (splice_start)
|
||||
|
||||
// TODO(splicing): The exisiting pubkey is reused, but a new one should be generated. See #3542.
|
||||
// Note that channel_keys_id is supposed NOT to change
|
||||
let splice_ack_msg = msgs::SpliceAck {
|
||||
channel_id: self.context.channel_id,
|
||||
funding_contribution_satoshis: our_funding_contribution_satoshis,
|
||||
funding_pubkey: self.funding.get_holder_pubkeys().funding_pubkey,
|
||||
require_confirmed_inputs: None,
|
||||
};
|
||||
// TODO(splicing): start interactive funding negotiation
|
||||
Ok(splice_ack_msg)
|
||||
}
|
||||
|
||||
/// Handle splice_ack
|
||||
#[cfg(splicing)]
|
||||
pub fn splice_ack(&mut self, _msg: &msgs::SpliceAck) -> Result<(), ChannelError> {
|
||||
// check if splice is pending
|
||||
if self.pending_splice.is_none() {
|
||||
return Err(ChannelError::Warn(format!("Channel is not in pending splice")));
|
||||
};
|
||||
|
||||
// TODO(splicing): Pre-check for reserve requirement
|
||||
// (Note: It should also be checked later at tx_complete)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Send stuff to our remote peers:
|
||||
|
||||
|
@ -9282,6 +9483,8 @@ impl<SP: Deref> OutboundV1Channel<SP> where SP::Target: SignerProvider {
|
|||
interactive_tx_signing_session: None,
|
||||
is_v2_established: false,
|
||||
holder_commitment_point,
|
||||
#[cfg(splicing)]
|
||||
pending_splice: None,
|
||||
};
|
||||
|
||||
let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some()
|
||||
|
@ -9549,6 +9752,8 @@ impl<SP: Deref> InboundV1Channel<SP> where SP::Target: SignerProvider {
|
|||
interactive_tx_signing_session: None,
|
||||
is_v2_established: false,
|
||||
holder_commitment_point,
|
||||
#[cfg(splicing)]
|
||||
pending_splice: None,
|
||||
};
|
||||
let need_channel_ready = channel.check_get_channel_ready(0, logger).is_some()
|
||||
|| channel.context.signer_pending_channel_ready;
|
||||
|
@ -10909,6 +11114,8 @@ impl<'a, 'b, 'c, ES: Deref, SP: Deref> ReadableArgs<(&'a ES, &'b SP, &'c Channel
|
|||
interactive_tx_signing_session: None,
|
||||
is_v2_established,
|
||||
holder_commitment_point,
|
||||
#[cfg(splicing)]
|
||||
pending_splice: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -10920,8 +11127,12 @@ mod tests {
|
|||
use bitcoin::constants::ChainHash;
|
||||
use bitcoin::script::{ScriptBuf, Builder};
|
||||
use bitcoin::transaction::{Transaction, TxOut, Version};
|
||||
#[cfg(splicing)]
|
||||
use bitcoin::transaction::TxIn;
|
||||
use bitcoin::opcodes;
|
||||
use bitcoin::network::Network;
|
||||
#[cfg(splicing)]
|
||||
use bitcoin::Weight;
|
||||
use crate::ln::onion_utils::INVALID_ONION_BLINDING;
|
||||
use crate::types::payment::{PaymentHash, PaymentPreimage};
|
||||
use crate::ln::channel_keys::{RevocationKey, RevocationBasepoint};
|
||||
|
@ -12726,4 +12937,112 @@ mod tests {
|
|||
320
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(splicing)]
|
||||
fn funding_input_sats(input_value_sats: u64) -> (TxIn, Transaction, Weight) {
|
||||
use crate::sign::P2WPKH_WITNESS_WEIGHT;
|
||||
|
||||
let input_1_prev_out = TxOut { value: Amount::from_sat(input_value_sats), script_pubkey: ScriptBuf::default() };
|
||||
let input_1_prev_tx = Transaction {
|
||||
input: vec![], output: vec![input_1_prev_out],
|
||||
version: Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO,
|
||||
};
|
||||
let input_1_txin = TxIn {
|
||||
previous_output: bitcoin::OutPoint { txid: input_1_prev_tx.compute_txid(), vout: 0 },
|
||||
..Default::default()
|
||||
};
|
||||
(input_1_txin, input_1_prev_tx, Weight::from_wu(P2WPKH_WITNESS_WEIGHT))
|
||||
}
|
||||
|
||||
#[cfg(splicing)]
|
||||
#[test]
|
||||
fn test_check_v2_funding_inputs_sufficient() {
|
||||
use crate::ln::channel::check_v2_funding_inputs_sufficient;
|
||||
|
||||
// positive case, inputs well over intended contribution
|
||||
assert_eq!(
|
||||
check_v2_funding_inputs_sufficient(
|
||||
220_000,
|
||||
&[
|
||||
funding_input_sats(200_000),
|
||||
funding_input_sats(100_000),
|
||||
],
|
||||
true,
|
||||
true,
|
||||
2000,
|
||||
).unwrap(),
|
||||
2268,
|
||||
);
|
||||
|
||||
// negative case, inputs clearly insufficient
|
||||
{
|
||||
let res = check_v2_funding_inputs_sufficient(
|
||||
220_000,
|
||||
&[
|
||||
funding_input_sats(100_000),
|
||||
],
|
||||
true,
|
||||
true,
|
||||
2000,
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{:?}", res.err().unwrap()),
|
||||
"Warn: Total input amount 100000 is lower than needed for contribution 220000, considering fees of 1730. Need more inputs.",
|
||||
);
|
||||
}
|
||||
|
||||
// barely covers
|
||||
{
|
||||
let expected_fee: u64 = 2268;
|
||||
assert_eq!(
|
||||
check_v2_funding_inputs_sufficient(
|
||||
(300_000 - expected_fee - 20) as i64,
|
||||
&[
|
||||
funding_input_sats(200_000),
|
||||
funding_input_sats(100_000),
|
||||
],
|
||||
true,
|
||||
true,
|
||||
2000,
|
||||
).unwrap(),
|
||||
expected_fee,
|
||||
);
|
||||
}
|
||||
|
||||
// higher fee rate, does not cover
|
||||
{
|
||||
let res = check_v2_funding_inputs_sufficient(
|
||||
298032,
|
||||
&[
|
||||
funding_input_sats(200_000),
|
||||
funding_input_sats(100_000),
|
||||
],
|
||||
true,
|
||||
true,
|
||||
2200,
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{:?}", res.err().unwrap()),
|
||||
"Warn: Total input amount 300000 is lower than needed for contribution 298032, considering fees of 2495. Need more inputs.",
|
||||
);
|
||||
}
|
||||
|
||||
// barely covers, less fees (no extra weight, no init)
|
||||
{
|
||||
let expected_fee: u64 = 1076;
|
||||
assert_eq!(
|
||||
check_v2_funding_inputs_sufficient(
|
||||
(300_000 - expected_fee - 20) as i64,
|
||||
&[
|
||||
funding_input_sats(200_000),
|
||||
funding_input_sats(100_000),
|
||||
],
|
||||
false,
|
||||
false,
|
||||
2000,
|
||||
).unwrap(),
|
||||
expected_fee,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ use bitcoin::hash_types::{BlockHash, Txid};
|
|||
use bitcoin::secp256k1::{SecretKey,PublicKey};
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use bitcoin::{secp256k1, Sequence};
|
||||
#[cfg(splicing)]
|
||||
use bitcoin::{TxIn, Weight};
|
||||
|
||||
use crate::events::FundingInfo;
|
||||
use crate::blinded_path::message::{AsyncPaymentsContext, MessageContext, OffersContext};
|
||||
|
@ -4294,6 +4296,86 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Initiate a splice, to change the channel capacity of an existing funded channel.
|
||||
/// After completion of splicing, the funding transaction will be replaced by a new one, spending the old funding transaction,
|
||||
/// with optional extra inputs (splice-in) and/or extra outputs (splice-out or change).
|
||||
/// TODO(splicing): Implementation is currently incomplete.
|
||||
///
|
||||
/// Note: Currently only splice-in is supported (increase in channel capacity), splice-out is not.
|
||||
///
|
||||
/// - `our_funding_contribution_satoshis`: the amount contributed by us to the channel. This will increase our channel balance.
|
||||
/// - `our_funding_inputs`: the funding inputs provided by us. If our contribution is positive, our funding inputs must cover at least that amount.
|
||||
/// Includes the witness weight for this input (e.g. P2WPKH_WITNESS_WEIGHT=109 for typical P2WPKH inputs).
|
||||
/// - `locktime`: Optional locktime for the new funding transaction. If None, set to the current block height.
|
||||
#[cfg(splicing)]
|
||||
pub fn splice_channel(
|
||||
&self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, our_funding_contribution_satoshis: i64,
|
||||
our_funding_inputs: Vec<(TxIn, Transaction, Weight)>,
|
||||
funding_feerate_per_kw: u32, locktime: Option<u32>,
|
||||
) -> Result<(), APIError> {
|
||||
let mut res = Ok(());
|
||||
PersistenceNotifierGuard::optionally_notify(self, || {
|
||||
let result = self.internal_splice_channel(
|
||||
channel_id, counterparty_node_id, our_funding_contribution_satoshis, &our_funding_inputs, funding_feerate_per_kw, locktime
|
||||
);
|
||||
res = result;
|
||||
match res {
|
||||
Ok(_) => NotifyOption::SkipPersistHandleEvents,
|
||||
Err(_) => NotifyOption::SkipPersistNoEvents,
|
||||
}
|
||||
});
|
||||
res
|
||||
}
|
||||
|
||||
/// See [`splice_channel`]
|
||||
#[cfg(splicing)]
|
||||
fn internal_splice_channel(
|
||||
&self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, our_funding_contribution_satoshis: i64,
|
||||
our_funding_inputs: &Vec<(TxIn, Transaction, Weight)>,
|
||||
funding_feerate_per_kw: u32, locktime: Option<u32>,
|
||||
) -> Result<(), APIError> {
|
||||
let per_peer_state = self.per_peer_state.read().unwrap();
|
||||
|
||||
let peer_state_mutex = match per_peer_state.get(counterparty_node_id)
|
||||
.ok_or_else(|| APIError::ChannelUnavailable { err: format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id) }) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
let mut peer_state_lock = peer_state_mutex.lock().unwrap();
|
||||
let peer_state = &mut *peer_state_lock;
|
||||
|
||||
// Look for the channel
|
||||
match peer_state.channel_by_id.entry(*channel_id) {
|
||||
hash_map::Entry::Occupied(mut chan_phase_entry) => {
|
||||
let locktime = locktime.unwrap_or_else(|| self.current_best_block().height);
|
||||
if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() {
|
||||
let msg = chan.splice_channel(our_funding_contribution_satoshis, our_funding_inputs, funding_feerate_per_kw, locktime)?;
|
||||
peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceInit {
|
||||
node_id: *counterparty_node_id,
|
||||
msg,
|
||||
});
|
||||
Ok(())
|
||||
} else {
|
||||
Err(APIError::ChannelUnavailable {
|
||||
err: format!(
|
||||
"Channel with id {} is not funded, cannot splice it",
|
||||
channel_id
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
hash_map::Entry::Vacant(_) => {
|
||||
Err(APIError::ChannelUnavailable {
|
||||
err: format!(
|
||||
"Channel with id {} not found for the passed counterparty node_id {}",
|
||||
channel_id, counterparty_node_id,
|
||||
)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn can_forward_htlc_to_outgoing_channel(
|
||||
&self, chan: &mut FundedChannel<SP>, msg: &msgs::UpdateAddHTLC, next_packet: &NextPacketDetails
|
||||
) -> Result<(), (&'static str, u16)> {
|
||||
|
@ -9471,6 +9553,81 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
|
|||
Ok(NotifyOption::SkipPersistHandleEvents)
|
||||
}
|
||||
|
||||
/// Handle incoming splice request, transition channel to splice-pending (unless some check fails).
|
||||
#[cfg(splicing)]
|
||||
fn internal_splice_init(&self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceInit) -> Result<(), MsgHandleErrInternal> {
|
||||
let per_peer_state = self.per_peer_state.read().unwrap();
|
||||
let peer_state_mutex = per_peer_state.get(counterparty_node_id)
|
||||
.ok_or_else(|| {
|
||||
debug_assert!(false);
|
||||
MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), msg.channel_id)
|
||||
})?;
|
||||
let mut peer_state_lock = peer_state_mutex.lock().unwrap();
|
||||
let peer_state = &mut *peer_state_lock;
|
||||
|
||||
// Look for the channel
|
||||
match peer_state.channel_by_id.entry(msg.channel_id) {
|
||||
hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!(
|
||||
"Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}, channel_id {}",
|
||||
counterparty_node_id, msg.channel_id,
|
||||
), msg.channel_id)),
|
||||
hash_map::Entry::Occupied(mut chan_entry) => {
|
||||
if let Some(chan) = chan_entry.get_mut().as_funded_mut() {
|
||||
let splice_ack_msg = try_channel_entry!(self, peer_state, chan.splice_init(msg), chan_entry);
|
||||
peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceAck {
|
||||
node_id: *counterparty_node_id,
|
||||
msg: splice_ack_msg,
|
||||
});
|
||||
} else {
|
||||
return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not funded, cannot be spliced".to_owned(), msg.channel_id));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// TODO(splicing):
|
||||
// Change channel, change phase (remove and add)
|
||||
// Create new post-splice channel
|
||||
// etc.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle incoming splice request ack, transition channel to splice-pending (unless some check fails).
|
||||
#[cfg(splicing)]
|
||||
fn internal_splice_ack(&self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceAck) -> Result<(), MsgHandleErrInternal> {
|
||||
let per_peer_state = self.per_peer_state.read().unwrap();
|
||||
let peer_state_mutex = per_peer_state.get(counterparty_node_id)
|
||||
.ok_or_else(|| {
|
||||
debug_assert!(false);
|
||||
MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {}", counterparty_node_id), msg.channel_id)
|
||||
})?;
|
||||
let mut peer_state_lock = peer_state_mutex.lock().unwrap();
|
||||
let peer_state = &mut *peer_state_lock;
|
||||
|
||||
// Look for the channel
|
||||
match peer_state.channel_by_id.entry(msg.channel_id) {
|
||||
hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!(
|
||||
"Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}",
|
||||
counterparty_node_id
|
||||
), msg.channel_id)),
|
||||
hash_map::Entry::Occupied(mut chan_entry) => {
|
||||
if let Some(chan) = chan_entry.get_mut().as_funded_mut() {
|
||||
try_channel_entry!(self, peer_state, chan.splice_ack(msg), chan_entry);
|
||||
} else {
|
||||
return Err(MsgHandleErrInternal::send_err_msg_no_close("Channel is not funded, cannot splice".to_owned(), msg.channel_id));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// TODO(splicing):
|
||||
// Change channel, change phase (remove and add)
|
||||
// Create new post-splice channel
|
||||
// Start splice funding transaction negotiation
|
||||
// etc.
|
||||
|
||||
Err(MsgHandleErrInternal::send_err_msg_no_close("TODO(splicing): Splicing is not implemented (splice_ack)".to_owned(), msg.channel_id))
|
||||
}
|
||||
|
||||
/// Process pending events from the [`chain::Watch`], returning whether any events were processed.
|
||||
fn process_pending_monitor_events(&self) -> bool {
|
||||
debug_assert!(self.total_consistency_lock.try_write().is_err()); // Caller holds read lock
|
||||
|
@ -11960,23 +12117,37 @@ where
|
|||
|
||||
#[cfg(splicing)]
|
||||
fn handle_splice_init(&self, counterparty_node_id: PublicKey, msg: &msgs::SpliceInit) {
|
||||
let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close(
|
||||
"Splicing not supported".to_owned(),
|
||||
msg.channel_id.clone())), counterparty_node_id);
|
||||
let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || {
|
||||
let res = self.internal_splice_init(&counterparty_node_id, msg);
|
||||
let persist = match &res {
|
||||
Err(e) if e.closes_channel() => NotifyOption::DoPersist,
|
||||
Err(_) => NotifyOption::SkipPersistHandleEvents,
|
||||
Ok(()) => NotifyOption::SkipPersistHandleEvents,
|
||||
};
|
||||
let _ = handle_error!(self, res, counterparty_node_id);
|
||||
persist
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(splicing)]
|
||||
fn handle_splice_ack(&self, counterparty_node_id: PublicKey, msg: &msgs::SpliceAck) {
|
||||
let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close(
|
||||
"Splicing not supported (splice_ack)".to_owned(),
|
||||
msg.channel_id.clone())), counterparty_node_id);
|
||||
let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || {
|
||||
let res = self.internal_splice_ack(&counterparty_node_id, msg);
|
||||
let persist = match &res {
|
||||
Err(e) if e.closes_channel() => NotifyOption::DoPersist,
|
||||
Err(_) => NotifyOption::SkipPersistHandleEvents,
|
||||
Ok(()) => NotifyOption::SkipPersistHandleEvents,
|
||||
};
|
||||
let _ = handle_error!(self, res, counterparty_node_id);
|
||||
persist
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(splicing)]
|
||||
fn handle_splice_locked(&self, counterparty_node_id: PublicKey, msg: &msgs::SpliceLocked) {
|
||||
let _: Result<(), _> = handle_error!(self, Err(MsgHandleErrInternal::send_err_msg_no_close(
|
||||
"Splicing not supported (splice_locked)".to_owned(),
|
||||
msg.channel_id.clone())), counterparty_node_id);
|
||||
msg.channel_id)), counterparty_node_id);
|
||||
}
|
||||
|
||||
fn handle_shutdown(&self, counterparty_node_id: PublicKey, msg: &msgs::Shutdown) {
|
||||
|
|
|
@ -49,7 +49,7 @@ fn do_test_v2_channel_establishment(session: V2ChannelEstablishmentTestSession)
|
|||
&[session.initiator_input_value_satoshis],
|
||||
)
|
||||
.into_iter()
|
||||
.map(|(txin, tx)| (txin, TransactionU16LenLimited::new(tx).unwrap()))
|
||||
.map(|(txin, tx, _)| (txin, TransactionU16LenLimited::new(tx).unwrap()))
|
||||
.collect();
|
||||
|
||||
// Alice creates a dual-funded channel as initiator.
|
||||
|
|
|
@ -36,7 +36,7 @@ use crate::util::test_utils;
|
|||
use crate::util::test_utils::{TestChainMonitor, TestScorer, TestKeysInterface};
|
||||
use crate::util::ser::{ReadableArgs, Writeable};
|
||||
|
||||
use bitcoin::WPubkeyHash;
|
||||
use bitcoin::{Weight, WPubkeyHash};
|
||||
use bitcoin::amount::Amount;
|
||||
use bitcoin::block::{Block, Header, Version as BlockVersion};
|
||||
use bitcoin::locktime::absolute::{LockTime, LOCK_TIME_THRESHOLD};
|
||||
|
@ -58,6 +58,7 @@ use core::mem;
|
|||
use core::ops::Deref;
|
||||
use crate::io;
|
||||
use crate::prelude::*;
|
||||
use crate::sign::P2WPKH_WITNESS_WEIGHT;
|
||||
use crate::sync::{Arc, Mutex, LockTestExt, RwLock};
|
||||
|
||||
pub const CHAN_CONFIRM_DEPTH: u32 = 10;
|
||||
|
@ -800,7 +801,7 @@ macro_rules! get_event_msg {
|
|||
assert_eq!(*node_id, $node_id);
|
||||
(*msg).clone()
|
||||
},
|
||||
_ => panic!("Unexpected event"),
|
||||
_ => panic!("Unexpected event {:?}", events[0]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1232,9 +1233,11 @@ fn internal_create_funding_transaction<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>,
|
|||
}
|
||||
}
|
||||
|
||||
/// Create test inputs for a funding transaction.
|
||||
/// Return the inputs (with prev tx), and the total witness weight for these inputs
|
||||
pub fn create_dual_funding_utxos_with_prev_txs(
|
||||
node: &Node<'_, '_, '_>, utxo_values_in_satoshis: &[u64],
|
||||
) -> Vec<(TxIn, Transaction)> {
|
||||
) -> Vec<(TxIn, Transaction, Weight)> {
|
||||
// Ensure we have unique transactions per node by using the locktime.
|
||||
let tx = Transaction {
|
||||
version: TxVersion::TWO,
|
||||
|
@ -1247,9 +1250,9 @@ pub fn create_dual_funding_utxos_with_prev_txs(
|
|||
}).collect()
|
||||
};
|
||||
|
||||
let mut result = vec![];
|
||||
let mut inputs = vec![];
|
||||
for i in 0..utxo_values_in_satoshis.len() {
|
||||
result.push(
|
||||
inputs.push(
|
||||
(TxIn {
|
||||
previous_output: OutPoint {
|
||||
txid: tx.compute_txid(),
|
||||
|
@ -1258,9 +1261,13 @@ pub fn create_dual_funding_utxos_with_prev_txs(
|
|||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence::ZERO,
|
||||
witness: Witness::new(),
|
||||
}, tx.clone()));
|
||||
},
|
||||
tx.clone(),
|
||||
Weight::from_wu(P2WPKH_WITNESS_WEIGHT),
|
||||
));
|
||||
}
|
||||
result
|
||||
|
||||
inputs
|
||||
}
|
||||
|
||||
pub fn sign_funding_transaction<'a, 'b, 'c>(node_a: &Node<'a, 'b, 'c>, node_b: &Node<'a, 'b, 'c>, channel_value: u64, expected_temporary_channel_id: ChannelId) -> Transaction {
|
||||
|
|
|
@ -61,6 +61,9 @@ mod async_payments_tests;
|
|||
#[cfg(any(test, feature = "_externalize_tests"))]
|
||||
#[allow(unused_mut)]
|
||||
pub mod functional_tests;
|
||||
#[cfg(all(test, splicing))]
|
||||
#[allow(unused_mut)]
|
||||
mod splicing_tests;
|
||||
#[cfg(test)]
|
||||
#[allow(unused_mut)]
|
||||
mod max_payment_path_len_tests;
|
||||
|
|
|
@ -469,7 +469,7 @@ pub struct SpliceInit {
|
|||
/// or remove from its channel balance (splice-out).
|
||||
pub funding_contribution_satoshis: i64,
|
||||
/// The feerate for the new funding transaction, set by the splice initiator
|
||||
pub funding_feerate_perkw: u32,
|
||||
pub funding_feerate_per_kw: u32,
|
||||
/// The locktime for the new funding transaction
|
||||
pub locktime: u32,
|
||||
/// The key of the sender (splice initiator) controlling the new funding transaction
|
||||
|
@ -2501,7 +2501,7 @@ impl_writeable_msg!(Stfu, {
|
|||
impl_writeable_msg!(SpliceInit, {
|
||||
channel_id,
|
||||
funding_contribution_satoshis,
|
||||
funding_feerate_perkw,
|
||||
funding_feerate_per_kw,
|
||||
locktime,
|
||||
funding_pubkey,
|
||||
}, {
|
||||
|
@ -4361,7 +4361,7 @@ mod tests {
|
|||
let splice_init = msgs::SpliceInit {
|
||||
channel_id: ChannelId::from_bytes([2; 32]),
|
||||
funding_contribution_satoshis: -123456,
|
||||
funding_feerate_perkw: 2000,
|
||||
funding_feerate_per_kw: 2000,
|
||||
locktime: 0,
|
||||
funding_pubkey: pubkey_1,
|
||||
require_confirmed_inputs: Some(()),
|
||||
|
|
159
lightning/src/ln/splicing_tests.rs
Normal file
159
lightning/src/ln/splicing_tests.rs
Normal file
|
@ -0,0 +1,159 @@
|
|||
// This file is Copyright its original authors, visible in version control
|
||||
// history.
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
use crate::ln::functional_test_utils::*;
|
||||
use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent};
|
||||
|
||||
/// Splicing test, simple splice-in flow. Starts with opening a V1 channel first.
|
||||
/// Builds on test_channel_open_simple()
|
||||
#[test]
|
||||
fn test_v1_splice_in() {
|
||||
let chanmon_cfgs = create_chanmon_cfgs(2);
|
||||
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
|
||||
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
|
||||
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
|
||||
|
||||
// Initiator and Acceptor nodes
|
||||
let initiator_node_index = 0;
|
||||
let acceptor_node_index = 1;
|
||||
let initiator_node = &nodes[initiator_node_index];
|
||||
let acceptor_node = &nodes[acceptor_node_index];
|
||||
|
||||
// Instantiate channel parameters where we push the maximum msats given our funding satoshis
|
||||
let channel_value_sat = 100_000; // same as funding satoshis
|
||||
let push_msat = 0;
|
||||
let channel_reserve_amnt_sat = 1_000;
|
||||
|
||||
let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(
|
||||
&nodes,
|
||||
initiator_node_index,
|
||||
acceptor_node_index,
|
||||
channel_value_sat,
|
||||
push_msat,
|
||||
);
|
||||
|
||||
let expected_funded_channel_id =
|
||||
"ae3367da2c13bc1ceb86bf56418f62828f7ce9d6bfb15a46af5ba1f1ed8b124f";
|
||||
assert_eq!(channel_id.to_string(), expected_funded_channel_id);
|
||||
|
||||
let expected_initiator_funding_key =
|
||||
"03c21e841cbc0b48197d060c71e116c185fa0ac281b7d0aa5924f535154437ca3b";
|
||||
let expected_acceptor_funding_key =
|
||||
"039481c28b904cbe12681e79937373fc76245c1b29871028ae60ba3152162c319b";
|
||||
|
||||
// ==== Channel is now ready for normal operation
|
||||
|
||||
// === Start of Splicing
|
||||
println!("Start of Splicing ..., channel_id {}", channel_id);
|
||||
|
||||
// Amount being added to the channel through the splice-in
|
||||
let splice_in_sats: u64 = 20000;
|
||||
let funding_feerate_per_kw = 1024; // TODO
|
||||
|
||||
// Create additional inputs
|
||||
let extra_splice_funding_input_sats = 35_000;
|
||||
let funding_inputs = create_dual_funding_utxos_with_prev_txs(
|
||||
&initiator_node,
|
||||
&[extra_splice_funding_input_sats],
|
||||
);
|
||||
// Initiate splice-in (on initiator_node)
|
||||
let _res = initiator_node
|
||||
.node
|
||||
.splice_channel(
|
||||
&channel_id,
|
||||
&acceptor_node.node.get_our_node_id(),
|
||||
splice_in_sats as i64,
|
||||
funding_inputs,
|
||||
funding_feerate_per_kw,
|
||||
None, // locktime
|
||||
)
|
||||
.unwrap();
|
||||
// Extract the splice message from node0 to node1
|
||||
let splice_init_msg = get_event_msg!(
|
||||
initiator_node,
|
||||
MessageSendEvent::SendSpliceInit,
|
||||
acceptor_node.node.get_our_node_id()
|
||||
);
|
||||
assert_eq!(splice_init_msg.funding_contribution_satoshis, splice_in_sats as i64);
|
||||
assert_eq!(splice_init_msg.funding_feerate_per_kw, funding_feerate_per_kw);
|
||||
assert_eq!(splice_init_msg.funding_pubkey.to_string(), expected_initiator_funding_key);
|
||||
assert!(splice_init_msg.require_confirmed_inputs.is_none());
|
||||
|
||||
let _res = acceptor_node
|
||||
.node
|
||||
.handle_splice_init(initiator_node.node.get_our_node_id(), &splice_init_msg);
|
||||
// Extract the splice_ack message from node1 to node0
|
||||
let splice_ack_msg = get_event_msg!(
|
||||
acceptor_node,
|
||||
MessageSendEvent::SendSpliceAck,
|
||||
initiator_node.node.get_our_node_id()
|
||||
);
|
||||
assert_eq!(splice_ack_msg.funding_contribution_satoshis, 0);
|
||||
assert_eq!(splice_ack_msg.funding_pubkey.to_string(), expected_acceptor_funding_key);
|
||||
assert!(splice_ack_msg.require_confirmed_inputs.is_none());
|
||||
|
||||
// still pre-splice channel: capacity not updated, channel usable, and funding tx set
|
||||
assert_eq!(acceptor_node.node.list_channels().len(), 1);
|
||||
{
|
||||
let channel = &acceptor_node.node.list_channels()[0];
|
||||
assert_eq!(channel.channel_id.to_string(), expected_funded_channel_id);
|
||||
assert!(channel.is_usable);
|
||||
assert!(channel.is_channel_ready);
|
||||
assert_eq!(channel.channel_value_satoshis, channel_value_sat);
|
||||
assert_eq!(channel.outbound_capacity_msat, 0);
|
||||
assert!(channel.funding_txo.is_some());
|
||||
assert!(channel.confirmations.unwrap() > 0);
|
||||
}
|
||||
|
||||
let _res = initiator_node
|
||||
.node
|
||||
.handle_splice_ack(acceptor_node.node.get_our_node_id(), &splice_ack_msg);
|
||||
|
||||
// still pre-splice channel: capacity not updated, channel usable, and funding tx set
|
||||
assert_eq!(initiator_node.node.list_channels().len(), 1);
|
||||
{
|
||||
let channel = &initiator_node.node.list_channels()[0];
|
||||
assert_eq!(channel.channel_id.to_string(), expected_funded_channel_id);
|
||||
assert!(channel.is_usable);
|
||||
assert!(channel.is_channel_ready);
|
||||
assert_eq!(channel.channel_value_satoshis, channel_value_sat);
|
||||
assert_eq!(
|
||||
channel.outbound_capacity_msat,
|
||||
1000 * (channel_value_sat - channel_reserve_amnt_sat)
|
||||
);
|
||||
assert!(channel.funding_txo.is_some());
|
||||
assert!(channel.confirmations.unwrap() > 0);
|
||||
}
|
||||
|
||||
let _error_msg = get_err_msg(initiator_node, &acceptor_node.node.get_our_node_id());
|
||||
|
||||
// TODO(splicing): continue with splice transaction negotiation
|
||||
|
||||
// === Close channel, cooperatively
|
||||
initiator_node.node.close_channel(&channel_id, &acceptor_node.node.get_our_node_id()).unwrap();
|
||||
let node0_shutdown_message = get_event_msg!(
|
||||
initiator_node,
|
||||
MessageSendEvent::SendShutdown,
|
||||
acceptor_node.node.get_our_node_id()
|
||||
);
|
||||
acceptor_node
|
||||
.node
|
||||
.handle_shutdown(initiator_node.node.get_our_node_id(), &node0_shutdown_message);
|
||||
let nodes_1_shutdown = get_event_msg!(
|
||||
acceptor_node,
|
||||
MessageSendEvent::SendShutdown,
|
||||
initiator_node.node.get_our_node_id()
|
||||
);
|
||||
initiator_node.node.handle_shutdown(acceptor_node.node.get_our_node_id(), &nodes_1_shutdown);
|
||||
let _ = get_event_msg!(
|
||||
initiator_node,
|
||||
MessageSendEvent::SendClosingSigned,
|
||||
acceptor_node.node.get_our_node_id()
|
||||
);
|
||||
}
|
Loading…
Add table
Reference in a new issue