diff --git a/src/builder.rs b/src/builder.rs index 3df594b7c..3d9b0d742 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -64,7 +64,9 @@ use crate::io::utils::{ }; use crate::io::vss_store::VssStoreBuilder; use crate::io::{ - self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + self, CHANNEL_RECORD_PERSISTENCE_PRIMARY_NAMESPACE, + CHANNEL_RECORD_PERSISTENCE_SECONDARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, }; @@ -77,9 +79,9 @@ use crate::peer_store::PeerStore; use crate::runtime::{Runtime, RuntimeSpawner}; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ - AsyncPersister, ChainMonitor, ChannelManager, DynStore, DynStoreRef, DynStoreWrapper, - GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger, PaymentStore, - PeerManager, PendingPaymentStore, + AsyncPersister, ChainMonitor, ChannelManager, ChannelRecordStore, DynStore, DynStoreRef, + DynStoreWrapper, GossipSync, Graph, HRNResolver, KeysManager, MessageRouter, OnionMessenger, + PaymentStore, PeerManager, PendingPaymentStore, }; use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; @@ -1379,7 +1381,7 @@ fn build_with_store_internal( let kv_store_ref = Arc::clone(&kv_store); let logger_ref = Arc::clone(&logger); - let (payment_store_res, node_metris_res, pending_payment_store_res) = + let (payment_store_res, node_metris_res, pending_payment_store_res, channel_record_store_res) = runtime.block_on(async move { tokio::join!( read_all_objects( @@ -1394,6 +1396,12 @@ fn build_with_store_internal( PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, Arc::clone(&logger_ref), + ), + read_all_objects( + &*kv_store_ref, + CHANNEL_RECORD_PERSISTENCE_PRIMARY_NAMESPACE, + CHANNEL_RECORD_PERSISTENCE_SECONDARY_NAMESPACE, + Arc::clone(&logger_ref), ) ) }); @@ -1605,6 +1613,20 @@ fn build_with_store_internal( }, }; + let channel_record_store = match channel_record_store_res { + Ok(channel_records) => Arc::new(ChannelRecordStore::new( + channel_records, + CHANNEL_RECORD_PERSISTENCE_PRIMARY_NAMESPACE.to_string(), + CHANNEL_RECORD_PERSISTENCE_SECONDARY_NAMESPACE.to_string(), + Arc::clone(&kv_store), + Arc::clone(&logger), + )), + Err(e) => { + log_error!(logger, "Failed to read channel record data from store: {}", e); + return Err(BuildError::ReadFailed); + }, + }; + let wallet = Arc::new(Wallet::new( bdk_wallet, wallet_persister, @@ -2149,6 +2171,7 @@ fn build_with_store_internal( scorer, peer_store, payment_store, + channel_record_store, lnurl_auth, is_running, node_metrics, diff --git a/src/channel/mod.rs b/src/channel/mod.rs new file mode 100644 index 000000000..95a9f5224 --- /dev/null +++ b/src/channel/mod.rs @@ -0,0 +1,10 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +//! Per-channel state tracking. + +pub(crate) mod store; diff --git a/src/channel/store.rs b/src/channel/store.rs new file mode 100644 index 000000000..8619be009 --- /dev/null +++ b/src/channel/store.rs @@ -0,0 +1,125 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use bitcoin::secp256k1::PublicKey; +use lightning::impl_writeable_tlv_based_enum; +use lightning::ln::types::ChannelId; + +use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate}; +use crate::hex_utils; +use crate::types::UserChannelId; + +/// Persistent per-channel state tracked by LDK Node, keyed by [`UserChannelId`]. +/// +/// Tracks where a channel sits in its lifecycle. Each variant currently holds only the channel's +/// identity; the per-feature state and the transitions between lifecycle states are added by the +/// work that needs them. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum ChannelRecord { + /// A channel whose funding transaction has not yet been observed. + Unfunded { + user_channel_id: UserChannelId, + counterparty_node_id: PublicKey, + channel_id: ChannelId, + }, + /// A channel whose funding transaction exists. + Funded { + user_channel_id: UserChannelId, + counterparty_node_id: PublicKey, + channel_id: ChannelId, + }, + /// A channel that has been closed. + Closed { + user_channel_id: UserChannelId, + counterparty_node_id: PublicKey, + channel_id: ChannelId, + }, +} + +impl_writeable_tlv_based_enum!(ChannelRecord, + (0, Unfunded) => { + (0, user_channel_id, required), + (2, counterparty_node_id, required), + (4, channel_id, required), + }, + (2, Funded) => { + (0, user_channel_id, required), + (2, counterparty_node_id, required), + (4, channel_id, required), + }, + (4, Closed) => { + (0, user_channel_id, required), + (2, counterparty_node_id, required), + (4, channel_id, required), + }, +); + +impl StorableObjectId for UserChannelId { + fn encode_to_hex_str(&self) -> String { + hex_utils::to_string(&self.0.to_be_bytes()) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct ChannelRecordUpdate { + pub user_channel_id: UserChannelId, +} + +impl StorableObject for ChannelRecord { + type Id = UserChannelId; + type Update = ChannelRecordUpdate; + + fn id(&self) -> Self::Id { + match self { + ChannelRecord::Unfunded { user_channel_id, .. } + | ChannelRecord::Funded { user_channel_id, .. } + | ChannelRecord::Closed { user_channel_id, .. } => *user_channel_id, + } + } + + fn update(&mut self, _update: Self::Update) -> bool { + // Records currently carry only the channel's identity, so there is nothing to mutate in + // place. Lifecycle transitions are performed by replacing the record once a consumer of + // this store exists. + false + } + + fn to_update(&self) -> Self::Update { + Self::Update { user_channel_id: self.id() } + } +} + +impl StorableObjectUpdate for ChannelRecordUpdate { + fn id(&self) -> ::Id { + self.user_channel_id + } +} + +#[cfg(test)] +mod tests { + use lightning::util::ser::{Readable, Writeable}; + + use super::*; + + #[test] + fn channel_record_is_serializable() { + let user_channel_id = UserChannelId(42); + let counterparty_node_id = bitcoin::secp256k1::PublicKey::from_slice(&[2u8; 33]).unwrap(); + let channel_id = ChannelId([3u8; 32]); + + for record in [ + ChannelRecord::Unfunded { user_channel_id, counterparty_node_id, channel_id }, + ChannelRecord::Funded { user_channel_id, counterparty_node_id, channel_id }, + ChannelRecord::Closed { user_channel_id, counterparty_node_id, channel_id }, + ] { + let encoded = record.encode(); + let decoded = ChannelRecord::read(&mut &encoded[..]).unwrap(); + assert_eq!(record, decoded); + assert_eq!(decoded.id(), user_channel_id); + } + } +} diff --git a/src/io/mod.rs b/src/io/mod.rs index e16a99975..df974dfe1 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -84,3 +84,7 @@ pub(crate) const STATIC_INVOICE_STORE_PRIMARY_NAMESPACE: &str = "static_invoices /// The pending payment information will be persisted under this prefix. pub(crate) const PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "pending_payments"; pub(crate) const PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; + +/// The per-channel records will be persisted under this prefix. +pub(crate) const CHANNEL_RECORD_PERSISTENCE_PRIMARY_NAMESPACE: &str = "channel_records"; +pub(crate) const CHANNEL_RECORD_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; diff --git a/src/lib.rs b/src/lib.rs index b45064287..f518aa17c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,6 +83,7 @@ mod balance; mod builder; mod chain; +mod channel; pub mod config; mod connection; mod data_store; @@ -175,9 +176,9 @@ use peer_store::{PeerInfo, PeerStore}; use runtime::Runtime; pub use tokio; use types::{ - Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph, - HRNResolver, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, - Wallet, + Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, ChannelRecordStore, + DynStore, Graph, HRNResolver, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, + Scorer, Sweeper, Wallet, }; pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, UserChannelId}; pub use vss_client; @@ -242,6 +243,10 @@ pub struct Node { scorer: Arc>, peer_store: Arc>>, payment_store: Arc, + // Foundational per-channel record store, loaded and persisted here so that upcoming + // per-channel features can build on it. Not yet read within this crate. + #[allow(dead_code)] + channel_record_store: Arc, lnurl_auth: Arc, is_running: Arc>, node_metrics: Arc, diff --git a/src/types.rs b/src/types.rs index 64209430b..49c19e723 100644 --- a/src/types.rs +++ b/src/types.rs @@ -38,6 +38,7 @@ use lightning_net_tokio::SocketDescriptor; use crate::chain::bitcoind::UtxoSourceClient; use crate::chain::ChainSource; +use crate::channel::store::ChannelRecord; use crate::config::ChannelConfig; use crate::data_store::DataStore; use crate::fee_estimator::OnchainFeeEstimator; @@ -318,7 +319,7 @@ pub(crate) type PaymentStore = DataStore>; /// A local, potentially user-provided, identifier of a channel. /// /// By default, this will be randomly generated for the user to ensure local uniqueness. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct UserChannelId(pub u128); impl Writeable for UserChannelId { @@ -628,3 +629,5 @@ impl From<&(u64, Vec)> for CustomTlvRecord { } pub(crate) type PendingPaymentStore = DataStore>; + +pub(crate) type ChannelRecordStore = DataStore>;