diff --git a/smite-ir/src/mutators/operation_param.rs b/smite-ir/src/mutators/operation_param.rs index b5152f6c..da56a163 100644 --- a/smite-ir/src/mutators/operation_param.rs +++ b/smite-ir/src/mutators/operation_param.rs @@ -122,6 +122,7 @@ fn mutate_operation(op: &mut Operation, rng: &mut impl Rng) -> bool { | Operation::RecvAcceptChannel | Operation::RecvFundingSigned | Operation::RecvChannelReady + | Operation::Reconnect | Operation::BroadcastTransaction | Operation::LookupShortChannelId => { unreachable!("is_param_mutable returned true for {op:?}") diff --git a/smite-ir/src/operation.rs b/smite-ir/src/operation.rs index 22a17899..e7470237 100644 --- a/smite-ir/src/operation.rs +++ b/smite-ir/src/operation.rs @@ -228,6 +228,9 @@ pub enum Operation { /// point unknown) and its funding transaction has enough confirmations for /// the target to have sent `channel_ready`. RecvChannelReady, + /// Close the connection and establish a fresh one to the target, re-running + /// the Noise handshake and exchanging `init`. + Reconnect, /// Mines the given number of blocks on the Bitcoin network. MineBlocks(u8), /// Sign wallet inputs of the transaction and broadcast it via `bitcoin-cli`. @@ -556,6 +559,7 @@ impl fmt::Display for Operation { Self::RecvAcceptChannel => write!(f, "RecvAcceptChannel"), Self::RecvFundingSigned => write!(f, "RecvFundingSigned"), Self::RecvChannelReady => write!(f, "RecvChannelReady()"), + Self::Reconnect => write!(f, "Reconnect()"), Self::MineBlocks(v) => write!(f, "MineBlocks({v})"), Self::BroadcastTransaction => write!(f, "BroadcastTransaction"), Self::LookupShortChannelId => write!(f, "LookupShortChannelId"), @@ -595,6 +599,7 @@ impl Operation { Self::SendMessage | Self::SendChannelReady { .. } | Self::RecvChannelReady + | Self::Reconnect | Self::MineBlocks(_) | Self::BroadcastTransaction => None, Self::SendOpenChannel => Some(VariableType::SentOpenChannel), @@ -626,6 +631,7 @@ impl Operation { | Self::LoadTargetPubkeyFromContext | Self::LoadChainHashFromContext | Self::RecvChannelReady + | Self::Reconnect | Self::MineBlocks(_) => vec![], Self::DerivePoint => vec![VariableType::PrivateKey], @@ -763,6 +769,7 @@ impl Operation { | Self::SendShutdown | Self::RecvFundingSigned | Self::RecvChannelReady + | Self::Reconnect | Self::MineBlocks(_) | Self::BroadcastTransaction | Self::LookupShortChannelId => vec![], @@ -812,6 +819,7 @@ impl Operation { | Self::RecvAcceptChannel | Self::RecvFundingSigned | Self::RecvChannelReady + | Self::Reconnect | Self::MineBlocks(_) | Self::BroadcastTransaction => true, } @@ -858,14 +866,16 @@ impl Operation { // contents change as transactions are created and broadcast. // `SendFundingCreated` builds its message from the recorded // negotiation and channel state. The `Recv` operations read - // whatever the target sends us. `MineBlocks` also mines whatever - // the private mempool holds, `BroadcastTransaction` dedups against - // it, and `LookupShortChannelId` reads chain state. + // whatever the target sends us. `Reconnect` re-establishes the + // connection and depends on live transport state. `MineBlocks` also + // mines whatever the private mempool holds, `BroadcastTransaction` + // dedups against it, and `LookupShortChannelId` reads chain state. Self::CreateFundingTransaction | Self::SendFundingCreated | Self::RecvAcceptChannel | Self::RecvFundingSigned | Self::RecvChannelReady + | Self::Reconnect | Self::MineBlocks(_) | Self::BroadcastTransaction | Self::LookupShortChannelId => false, @@ -919,6 +929,7 @@ impl Operation { | Self::RecvAcceptChannel | Self::RecvFundingSigned | Self::RecvChannelReady + | Self::Reconnect | Self::BroadcastTransaction | Self::LookupShortChannelId => false, } diff --git a/smite-ir/src/tests.rs b/smite-ir/src/tests.rs index 53fdd3cf..b6a34b98 100644 --- a/smite-ir/src/tests.rs +++ b/smite-ir/src/tests.rs @@ -678,6 +678,18 @@ fn postcard_roundtrip() { assert_eq!(program, decoded); } +#[test] +fn reconnect_operation() { + let op = Operation::Reconnect; + assert_eq!(op.input_types(), vec![]); + assert_eq!(op.output_type(), None); + assert!(!op.is_param_mutable()); + assert!(op.has_side_effects()); + assert!(!op.depends_only_on_inputs()); + assert!(!op.is_pure()); + assert_eq!(op.to_string(), "Reconnect()"); +} + #[test] fn mine_blocks_operation() { let op = Operation::MineBlocks(8); diff --git a/smite-scenarios/src/executor.rs b/smite-scenarios/src/executor.rs index bf6f6b90..7413354c 100644 --- a/smite-scenarios/src/executor.rs +++ b/smite-scenarios/src/executor.rs @@ -9,9 +9,9 @@ use bitcoin::{OutPoint, ScriptBuf, Txid}; use smite::bitcoin::{BitcoinCli, TxBlockPosition, Utxo}; use smite::bolt::{ AcceptChannel, AnnouncementSignatures, ChannelAnnouncement, ChannelId, ChannelReady, - ChannelReadyTlvs, ChannelUpdate, Features, FromMessage, FundingCreated, FundingSigned, Message, - MessageType, NodeAnnouncement, OpenChannel, OpenChannelTlvs, Pong, ShortChannelId, Shutdown, - TemporaryChannelId, + ChannelReadyTlvs, ChannelUpdate, Features, FromMessage, FundingCreated, FundingSigned, Init, + InitTlvs, Message, MessageType, NodeAnnouncement, OpenChannel, OpenChannelTlvs, Pong, + ShortChannelId, Shutdown, TemporaryChannelId, }; use smite::channel_tx::{ ChannelConfig, ChannelPartyConfig, ChannelState, FundingTransaction, HolderIdentity, Side, @@ -175,6 +175,14 @@ pub trait Connection { /// /// Returns an error if the timeout cannot be read. fn read_timeout(&self) -> Result, ConnectionError>; + + /// Reconnects to the same remote Lightning node, establishing a new TCP + /// connection and performing the Noise handshake. + /// + /// # Errors + /// + /// Returns an error if the reconnect fails. + fn reconnect(&mut self) -> Result<(), ConnectionError>; } impl Connection for NoiseConnection { @@ -193,6 +201,10 @@ impl Connection for NoiseConnection { fn read_timeout(&self) -> Result, ConnectionError> { NoiseConnection::read_timeout(self) } + + fn reconnect(&mut self) -> Result<(), ConnectionError> { + NoiseConnection::reconnect(self) + } } /// Error from executing an IR program. @@ -538,6 +550,21 @@ impl Executor { None } + Operation::Reconnect => { + log::debug!("[{:?}] Reconnect: re-dialing", start.elapsed()); + self.conn.reconnect()?; + recv_bolt::(&mut self.conn, RECV_IDLE_TIMEOUT)?; + let our_init = build_init( + &mut self.channel_states, + &mut self.negotiations, + &self.context.negotiated_features, + ); + let encoded = Message::Init(our_init.clone()).encode(); + self.conn.send_message(&encoded)?; + log::debug!("[{:?}] Reconnect: init exchanged", start.elapsed()); + None + } + Operation::MineBlocks(v) => { // Clear the private mempool and mine the requested blocks, // adding those transactions to the first block. @@ -736,6 +763,50 @@ fn create_funding_transaction( Ok(funding) } +/// Builds the `init` message sent on (re)connect and updates the executor's +/// tracked state to match what the target retains across the reconnect. +/// +/// The `init` advertises the features negotiated on the original connection so +/// the peer retains the same view of the session. The state update then +/// discards every negotiation and channel that had not received +/// `funding_signed` before the reconnect: the target does not resend it, so +/// such entries can no longer progress and the target itself has forgotten them. +fn build_init( + channel_states: &mut HashMap, + negotiations: &mut HashMap, + negotiated_features: &Features, +) -> Init { + // BOLT 2: after reconnecting, the target discards any `open_channel` for + // which it has not yet received `funding_created`, allowing its + // `temporary_channel_id` to be reused for a new negotiation. + // + // Also discard negotiations whose `funding_signed` was not received before + // reconnect. The target will not resend it, so retaining such a negotiation + // would leave its channel state unable to progress. + negotiations.retain(|_, pending| { + pending + .funded_channel_id + .and_then(|id| channel_states.get(&id)) + .is_some_and(|state| state.funding_signed_received) + }); + + // BOLT 2: after reconnecting, the target remembers any channel for which it + // has already sent `funding_signed`. Preserve those channels and discard + // the rest, since `funding_signed` is not resent after reconnect and any + // buffered message from the previous connection is lost. + channel_states.retain(|_, state| state.funding_signed_received); + + // Put all negotiated features in `features` and leave the legacy + // `globalfeatures` empty. Peers combine both fields with a logical OR, so + // this is equivalent to splitting the features across the two fields and + // avoids advertising features greater than 13 in `globalfeatures`. + Init { + globalfeatures: Vec::new(), + features: negotiated_features.clone().into_bytes(), + tlvs: InitTlvs::default(), + } +} + /// Builds an `OpenChannel` from 20 input variables (wire order). fn build_open_channel(variables: &[Option], inputs: &[usize]) -> OpenChannel { OpenChannel { @@ -866,7 +937,7 @@ fn build_funding_created( // // This also means that building the same message again must not clobber a // channel whose state has already been established (and possibly advanced). - if !pending.funding_built { + if pending.funded_channel_id.is_none() { let channel_id = ChannelId::v1_from_funding_outpoint(config.funding_outpoint); // Check whether the funding outpoint is valid and contains the @@ -895,14 +966,15 @@ fn build_funding_created( sent_invalid_signature, ) }); - } - // Mark this negotiation as having built `funding_created`. It is retained - // so repeated `funding_created` messages can still be built, but a later - // `open_channel` reusing this `temporary_channel_id` starts a fresh - // negotiation. - if let Some(pending) = negotiations.get_mut(&temporary_channel_id) { - pending.funding_built = true; + // Record the channel ID from the funding outpoint. Retain this + // negotiation so repeated `funding_created` messages can still be built, + // while a later `open_channel` reusing this `temporary_channel_id` + // starts a fresh negotiation. + negotiations + .get_mut(&temporary_channel_id) + .expect("temporary_channel_id already exists") + .funded_channel_id = Some(channel_id); } Ok(FundingCreated { @@ -1216,6 +1288,11 @@ fn recv_channel_ready( ) -> Result<(), ExecuteError> { let cr: ChannelReady = recv_bolt(conn, RECV_CHANNEL_READY_TIMEOUT)?; + // FIXME: If the target sent `funding_signed` before reconnecting but we did + // not receive it, it may send `channel_ready` once the funding transaction + // reaches minimum depth. We would then flag it as an unknown channel. Check + // whether the channel maps to a funding transaction we broadcast that is in + // `mined_txids`, and discard the `channel_ready` in that case. let state = channel_states .get_mut(&cr.channel_id) .ok_or(Violation::UnknownChannel(cr.channel_id))?; @@ -1260,7 +1337,7 @@ fn record_send_open_channel( ) { if negotiations .get(&open_channel.temporary_channel_id) - .is_some_and(|pending| !pending.funding_built) + .is_some_and(|pending| pending.funded_channel_id.is_none()) { return; } @@ -1270,7 +1347,7 @@ fn record_send_open_channel( PendingChannel { open_channel: open_channel.clone(), accept_channel: None, - funding_built: false, + funded_channel_id: None, }, ); } diff --git a/smite-scenarios/src/executor/tests.rs b/smite-scenarios/src/executor/tests.rs index 3f030aa4..e9a72528 100644 --- a/smite-scenarios/src/executor/tests.rs +++ b/smite-scenarios/src/executor/tests.rs @@ -407,6 +407,64 @@ fn execute_recv_skips_gossip() { assert_eq!(accept_channel.clone(), sample_accept_channel()); } +#[test] +fn execute_reconnect_exchanges_init() { + // A stale negotiation that never reached `funding_signed` and so must be + // dropped on reconnect. + let stale_id = TemporaryChannelId::new([0xcc; 32]); + // A second UTXO so the program can build a second funding transaction. + let mut second_utxo = sample_utxo(); + second_utxo.amount = Amount::from_sat(20_010_000); + second_utxo.outpoint.vout = 1; + let mut stale = sample_funding_negotiation(); + stale.open_channel.temporary_channel_id = stale_id; + stale.open_channel.funding_satoshis = 20_000_000; + stale.accept_channel.as_mut().unwrap().temporary_channel_id = stale_id; + + // Create funding transactions for both negotiations and send + // `funding_created`. The first receives `funding_signed`, while the second + // does not and is therefore discarded on reconnect. + let mut b = ProgramBuilder::new(); + let funding_created_1 = send_funding_created(&mut b); + b.append(Operation::RecvFundingSigned, &[funding_created_1.sent]); + let tx = create_funding_tx_with(&mut b, 20_000_000, 15_000); + let stale_temp_chan_id = b.append(Operation::LoadChannelId(stale_id.0), &[]); + b.append( + Operation::SendFundingCreated, + &[tx.tx, tx.opener_privkey, stale_temp_chan_id], + ); + b.append(Operation::Reconnect, &[]); + + let mut fx = Fixture::new() + .with_utxos(vec![sample_utxo(), second_utxo]) + .with_negotiation(sample_funding_negotiation()) + .queue(&funding_signed_reply(funding_channel_id())) + .with_negotiation(stale) + .queue(&Message::Init(Init::empty())); + fx.run(&b.build()); + + // Every queued reply was consumed and the reconnect sent exactly one `init` + // (after the `funding_created` from the funding flow). + assert_eq!(fx.queued_len(), 0); + assert_eq!(fx.sent_len(), 3); + let sent: Init = fx.sent(2); + assert_eq!(sent.globalfeatures, Vec::::new()); + assert_eq!( + sent.features, + sample_context().negotiated_features.into_bytes() + ); + + // The funded channel and its negotiation survived the reconnect, the stale + // negotiation was discarded. + assert_eq!(fx.channel_states().len(), 1); + assert!(fx.channel_states().contains_key(&funding_channel_id())); + assert_eq!(fx.negotiations().len(), 1); + assert!( + fx.negotiations() + .contains_key(&TemporaryChannelId::new([0xbb; 32])) + ); +} + #[test] fn execute_records_negotiation_for_open_and_accept() { let temporary_channel_id = TemporaryChannelId::new([0xbb; 32]); @@ -421,7 +479,7 @@ fn execute_records_negotiation_for_open_and_accept() { ); let accept_channel = pending.accept_channel.as_ref().unwrap(); assert_eq!(accept_channel.clone(), sample_accept_channel()); - assert!(!pending.funding_built); + assert!(pending.funded_channel_id.is_none()); } #[test] @@ -541,7 +599,7 @@ fn execute_records_open_channel_for_duplicate_id_after_funding() { let pending = fx.negotiation(&temporary_channel_id); assert_eq!(pending.open_channel.funding_satoshis, 100_000); assert!(pending.accept_channel.is_none()); - assert!(!pending.funding_built); + assert!(pending.funded_channel_id.is_none()); } // -- Panic path tests -- @@ -792,7 +850,7 @@ fn execute_send_funding_created_and_recv_funding_signed() { ); let pending = fx.negotiation(&TemporaryChannelId::new([0xbb; 32])); - assert!(pending.funding_built); + assert!(pending.funded_channel_id.is_some()); assert_eq!(fx.rpc().chain_syncs, 0); } diff --git a/smite-scenarios/src/executor/tests/harness.rs b/smite-scenarios/src/executor/tests/harness.rs index 668e6f1a..4dce0369 100644 --- a/smite-scenarios/src/executor/tests/harness.rs +++ b/smite-scenarios/src/executor/tests/harness.rs @@ -41,6 +41,10 @@ impl Connection for MockConnection { fn read_timeout(&self) -> Result, ConnectionError> { Ok(None) } + + fn reconnect(&mut self) -> Result<(), ConnectionError> { + Ok(()) + } } // Mocking BitcoinCli via MockBitcoinCli @@ -205,6 +209,11 @@ impl Fixture { &self.executor.channel_states } + /// Returns every negotiation the executor recorded. + pub fn negotiations(&self) -> &HashMap { + &self.executor.negotiations + } + /// Returns the mock bitcoind the executor drives. pub fn bitcoin(&self) -> &MockBitcoinCli { &self.executor.bitcoin_cli @@ -504,6 +513,6 @@ pub fn sample_funding_negotiation() -> PendingChannel { first_per_commitment_point: acceptor_pk, tlvs: AcceptChannelTlvs::default(), }), - funding_built: false, + funded_channel_id: None, } } diff --git a/smite/src/noise/connection.rs b/smite/src/noise/connection.rs index 2dd89285..ec87f193 100644 --- a/smite/src/noise/connection.rs +++ b/smite/src/noise/connection.rs @@ -1,7 +1,7 @@ //! High-level encrypted connection for Lightning Network peers. use std::io::{Read, Write}; -use std::net::{SocketAddr, TcpStream}; +use std::net::{Shutdown, SocketAddr, TcpStream}; use std::time::Duration; use bitcoin::secp256k1::{PublicKey, SecretKey}; @@ -17,6 +17,7 @@ use super::handshake::{ACT_TWO_SIZE, NoiseHandshake}; pub struct NoiseConnection { stream: TcpStream, cipher: NoiseCipher, + connector: Connector, } impl NoiseConnection { @@ -39,37 +40,33 @@ impl NoiseConnection { local_ephemeral: SecretKey, timeout: Duration, ) -> Result { - let mut stream = TcpStream::connect_timeout(&addr, timeout)?; - stream.set_nodelay(true)?; - stream.set_read_timeout(Some(timeout))?; - stream.set_write_timeout(Some(timeout))?; - - let cipher = - Self::perform_handshake(&mut stream, local_static, local_ephemeral, remote_pubkey)?; - - Ok(Self { stream, cipher }) + let connector = Connector::new(addr, remote_pubkey, local_static, local_ephemeral, timeout); + let (stream, cipher) = connector.connect()?; + + Ok(Self { + stream, + cipher, + connector, + }) } - /// Performs the Noise handshake as initiator. - fn perform_handshake( - stream: &mut TcpStream, - local_static: SecretKey, - local_ephemeral: SecretKey, - remote_pubkey: PublicKey, - ) -> Result { - let mut handshake = - NoiseHandshake::new_initiator(local_static, local_ephemeral, remote_pubkey); - - let act_one = handshake.get_act_one()?; - stream.write_all(&act_one)?; - - let mut act_two = [0u8; ACT_TWO_SIZE]; - stream.read_exact(&mut act_two)?; + /// Reconnects to the same remote Lightning node, establishing a new TCP + /// connection and performing the Noise handshake. + /// + /// # Errors + /// + /// Returns an error if closing the current connection, establishing the new + /// connection, or completing the Noise handshake fails. + pub fn reconnect(&mut self) -> Result<(), ConnectionError> { + // Close the old connection before establishing the new one. + self.stream.shutdown(Shutdown::Both)?; - let act_three = handshake.process_act_two(&act_two)?; - stream.write_all(&act_three)?; + // Replace the stream and cipher with those from the new connection. + let (stream, cipher) = self.connector.connect()?; + self.stream = stream; + self.cipher = cipher; - Ok(handshake.into_cipher()?) + Ok(()) } /// Sends an encrypted message to the peer. @@ -128,6 +125,70 @@ impl NoiseConnection { } } +/// Stores the connection parameters needed to establish a Noise-encrypted +/// connection to a remote Lightning node. Retained by [`NoiseConnection`] to +/// reconnect to the same peer via [`reconnect`](NoiseConnection::reconnect). +struct Connector { + addr: SocketAddr, + remote_pubkey: PublicKey, + local_static: SecretKey, + local_ephemeral: SecretKey, + timeout: Duration, +} + +impl Connector { + /// Creates a connector with the address, keys, and timeout needed to + /// establish the connection. + fn new( + addr: SocketAddr, + remote_pubkey: PublicKey, + local_static: SecretKey, + local_ephemeral: SecretKey, + timeout: Duration, + ) -> Self { + Self { + addr, + remote_pubkey, + local_static, + local_ephemeral, + timeout, + } + } + + /// Opens a TCP connection to the remote Lightning node and performs the + /// Noise handshake, returning the connected stream and cipher. + fn connect(&self) -> Result<(TcpStream, NoiseCipher), ConnectionError> { + let mut stream = TcpStream::connect_timeout(&self.addr, self.timeout)?; + stream.set_nodelay(true)?; + stream.set_read_timeout(Some(self.timeout))?; + stream.set_write_timeout(Some(self.timeout))?; + + let cipher = self.perform_handshake(&mut stream)?; + + Ok((stream, cipher)) + } + + /// Performs the Noise handshake as initiator. + fn perform_handshake(&self, stream: &mut TcpStream) -> Result { + let mut handshake = NoiseHandshake::new_initiator( + self.local_static, + self.local_ephemeral, + self.remote_pubkey, + ); + + let act_one = handshake.get_act_one()?; + stream.write_all(&act_one)?; + + let mut act_two = [0u8; ACT_TWO_SIZE]; + stream.read_exact(&mut act_two)?; + + let act_three = handshake.process_act_two(&act_two)?; + stream.write_all(&act_three)?; + + Ok(handshake.into_cipher()?) + } +} + /// Errors that can occur during connection operations. #[derive(Debug, thiserror::Error)] pub enum ConnectionError { diff --git a/smite/src/oracles/accept_channel.rs b/smite/src/oracles/accept_channel.rs index 1a603c09..d0cb8dfe 100644 --- a/smite/src/oracles/accept_channel.rs +++ b/smite/src/oracles/accept_channel.rs @@ -54,7 +54,7 @@ impl Oracle> for AcceptChannelOracle { let Some(PendingChannel { open_channel, accept_channel: previous_accept_channel, - funding_built, + funded_channel_id, }) = context.negotiation else { return Err(Violation::InvalidAcceptChannel( @@ -86,7 +86,7 @@ impl Oracle> for AcceptChannelOracle { } // Check that the `temporary_channel_id` was not reused. - if previous_accept_channel.is_some() && !funding_built { + if previous_accept_channel.is_some() && funded_channel_id.is_none() { return Err(Violation::InvalidAcceptChannel( context.accept_channel.temporary_channel_id, "temporary_channel_id reuse: previous negotiation has not reached funding_created" @@ -444,7 +444,9 @@ fn max_accepted_htlcs_limit(channel_type: &Features) -> u16 { #[cfg(test)] mod tests { use super::*; - use crate::bolt::{AcceptChannelTlvs, CHAIN_HASH_SIZE, OpenChannelTlvs, TemporaryChannelId}; + use crate::bolt::{ + AcceptChannelTlvs, CHAIN_HASH_SIZE, ChannelId, OpenChannelTlvs, TemporaryChannelId, + }; use bitcoin::hashes::Hash; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use bitcoin::{PubkeyHash, ScriptBuf, WPubkeyHash}; @@ -513,7 +515,7 @@ mod tests { PendingChannel { open_channel: oc, accept_channel: None, - funding_built: false, + funded_channel_id: None, } } @@ -1238,7 +1240,7 @@ mod tests { fn temporary_channel_id_reuse_after_funding_created() { let mut negotiation = pending_negotiation(open_channel()); negotiation.accept_channel = Some(accept_channel()); - negotiation.funding_built = true; + negotiation.funded_channel_id = Some(ChannelId::ALL); assert_pass( &accept_channel(), diff --git a/smite/src/pending_channel.rs b/smite/src/pending_channel.rs index bacb467e..beb8a7e9 100644 --- a/smite/src/pending_channel.rs +++ b/smite/src/pending_channel.rs @@ -3,15 +3,17 @@ //! Remembers the `open_channel`/`accept_channel` parameters of each channel //! being established, so later steps can build commitments from them. -use crate::bolt::{AcceptChannel, OpenChannel}; +use crate::bolt::{AcceptChannel, ChannelId, OpenChannel}; /// Negotiation parameters for a channel being established. /// /// Contains the initiating peer's `open_channel` message, the corresponding -/// `accept_channel` once received, and whether a `funding_created` has already -/// been built from this negotiation. +/// `accept_channel` once received, and the resulting channel ID once +/// `funding_created` has been built from the negotiation. pub struct PendingChannel { pub open_channel: OpenChannel, pub accept_channel: Option, - pub funding_built: bool, + /// The channel ID derived from the funding outpoint once `funding_created` + /// has been built. `None` while the negotiation is still pre-funding. + pub funded_channel_id: Option, }