77
88use std:: time:: { Duration , SystemTime , UNIX_EPOCH } ;
99
10+ use bitcoin:: secp256k1:: PublicKey ;
1011use bitcoin:: { BlockHash , Txid } ;
12+ use lightning:: chain:: chaininterface:: TransactionType as LdkTransactionType ;
1113use lightning:: ln:: channelmanager:: PaymentId ;
1214use lightning:: ln:: msgs:: DecodeError ;
15+ use lightning:: ln:: types:: ChannelId ;
1316use lightning:: offers:: offer:: OfferId ;
1417use lightning:: util:: ser:: { Readable , Writeable } ;
1518use lightning:: {
@@ -22,6 +25,138 @@ use lightning_types::string::UntrustedString;
2225use crate :: data_store:: { StorableObject , StorableObjectId , StorableObjectUpdate } ;
2326use crate :: hex_utils;
2427
28+ /// A helper struct pairing a counterparty node id with a channel id.
29+ ///
30+ /// This is used in [`TransactionType`] variants that may reference one or more channels.
31+ #[ derive( Clone , Debug , Hash , PartialEq , Eq ) ]
32+ pub struct Channel {
33+ /// The `node_id` of the channel counterparty.
34+ pub counterparty_node_id : PublicKey ,
35+ /// The ID of the channel.
36+ pub channel_id : ChannelId ,
37+ }
38+
39+ impl_writeable_tlv_based ! ( Channel , {
40+ ( 0 , counterparty_node_id, required) ,
41+ ( 2 , channel_id, required) ,
42+ } ) ;
43+
44+ /// Represents the class of transaction being broadcast.
45+ ///
46+ /// This is used to provide context about the type of transaction being broadcast, which may be
47+ /// useful for logging, filtering, or prioritization purposes.
48+ #[ derive( Clone , Debug , PartialEq , Eq ) ]
49+ pub enum TransactionType {
50+ /// A funding transaction establishing a new channel.
51+ Funding {
52+ /// The counterparty node IDs and channel IDs of the channels being funded.
53+ ///
54+ /// A single funding transaction may establish multiple channels when using batch funding.
55+ channels : Vec < Channel > ,
56+ } ,
57+ /// A transaction cooperatively closing a channel.
58+ CooperativeClose {
59+ /// The `node_id` of the channel counterparty.
60+ counterparty_node_id : PublicKey ,
61+ /// The ID of the channel being closed.
62+ channel_id : ChannelId ,
63+ } ,
64+ /// A transaction being broadcast to force-close the channel.
65+ UnilateralClose {
66+ /// The `node_id` of the channel counterparty.
67+ counterparty_node_id : PublicKey ,
68+ /// The ID of the channel being force-closed.
69+ channel_id : ChannelId ,
70+ } ,
71+ /// An anchor bumping transaction used for CPFP fee-bumping a closing transaction.
72+ AnchorBump {
73+ /// The `node_id` of the channel counterparty.
74+ counterparty_node_id : PublicKey ,
75+ /// The ID of the channel whose closing transaction is being fee-bumped.
76+ channel_id : ChannelId ,
77+ } ,
78+ /// A transaction which is resolving an output spendable by both us and our counterparty.
79+ ///
80+ /// When a channel closes via the unilateral close path, there may be transaction outputs which
81+ /// are spendable by either our counterparty or us and represent some lightning state. In order
82+ /// to resolve that state, any such outputs will be spent, ensuring funds are only available to
83+ /// us. This transaction is one such transaction - resolving in-flight HTLCs or punishing our
84+ /// counterparty if they broadcasted an outdated state.
85+ Claim {
86+ /// The `node_id` of the channel counterparty.
87+ counterparty_node_id : PublicKey ,
88+ /// The ID of the channel from which outputs are being claimed.
89+ channel_id : ChannelId ,
90+ } ,
91+ /// A transaction sweeping spendable outputs to the user's wallet.
92+ Sweep {
93+ /// The counterparty node IDs and channel IDs from which outputs are being swept, if known.
94+ ///
95+ /// A single sweep transaction may aggregate outputs from multiple channels.
96+ channels : Vec < Channel > ,
97+ } ,
98+ /// A splice transaction modifying an existing channel's funding.
99+ Splice {
100+ /// The `node_id` of the channel counterparty.
101+ counterparty_node_id : PublicKey ,
102+ /// The ID of the channel being spliced.
103+ channel_id : ChannelId ,
104+ } ,
105+ /// A user-initiated on-chain payment to an external address.
106+ OnchainSend ,
107+ }
108+
109+ impl_writeable_tlv_based_enum ! ( TransactionType ,
110+ ( 0 , Funding ) => { ( 0 , channels, optional_vec) } ,
111+ ( 2 , CooperativeClose ) => { ( 0 , counterparty_node_id, required) , ( 2 , channel_id, required) } ,
112+ ( 4 , UnilateralClose ) => { ( 0 , counterparty_node_id, required) , ( 2 , channel_id, required) } ,
113+ ( 6 , AnchorBump ) => { ( 0 , counterparty_node_id, required) , ( 2 , channel_id, required) } ,
114+ ( 8 , Claim ) => { ( 0 , counterparty_node_id, required) , ( 2 , channel_id, required) } ,
115+ ( 10 , Sweep ) => { ( 0 , channels, optional_vec) } ,
116+ ( 12 , Splice ) => { ( 0 , counterparty_node_id, required) , ( 2 , channel_id, required) } ,
117+ ( 14 , OnchainSend ) => { }
118+ ) ;
119+
120+ impl From < LdkTransactionType > for TransactionType {
121+ fn from ( ldk_type : LdkTransactionType ) -> Self {
122+ match ldk_type {
123+ LdkTransactionType :: Funding { channels } => TransactionType :: Funding {
124+ channels : channels
125+ . into_iter ( )
126+ . map ( |( node_id, chan_id) | Channel {
127+ counterparty_node_id : node_id,
128+ channel_id : chan_id,
129+ } )
130+ . collect ( ) ,
131+ } ,
132+ LdkTransactionType :: CooperativeClose { counterparty_node_id, channel_id } => {
133+ TransactionType :: CooperativeClose { counterparty_node_id, channel_id }
134+ } ,
135+ LdkTransactionType :: UnilateralClose { counterparty_node_id, channel_id } => {
136+ TransactionType :: UnilateralClose { counterparty_node_id, channel_id }
137+ } ,
138+ LdkTransactionType :: AnchorBump { counterparty_node_id, channel_id } => {
139+ TransactionType :: AnchorBump { counterparty_node_id, channel_id }
140+ } ,
141+ LdkTransactionType :: Claim { counterparty_node_id, channel_id } => {
142+ TransactionType :: Claim { counterparty_node_id, channel_id }
143+ } ,
144+ LdkTransactionType :: Sweep { channels } => TransactionType :: Sweep {
145+ channels : channels
146+ . into_iter ( )
147+ . map ( |( node_id, chan_id) | Channel {
148+ counterparty_node_id : node_id,
149+ channel_id : chan_id,
150+ } )
151+ . collect ( ) ,
152+ } ,
153+ LdkTransactionType :: Splice { counterparty_node_id, channel_id } => {
154+ TransactionType :: Splice { counterparty_node_id, channel_id }
155+ } ,
156+ }
157+ }
158+ }
159+
25160/// Represents a payment.
26161#[ derive( Clone , Debug , PartialEq , Eq ) ]
27162pub struct PaymentDetails {
@@ -291,6 +426,19 @@ impl StorableObject for PaymentDetails {
291426 }
292427 }
293428
429+ if let Some ( Some ( ref new_tx_type) ) = update. tx_type {
430+ match self . kind {
431+ PaymentKind :: Onchain { ref mut tx_type, .. } => {
432+ debug_assert ! (
433+ tx_type. is_none( ) || tx_type. as_ref( ) == Some ( new_tx_type) ,
434+ "We should never change a transaction type after being initially set"
435+ ) ;
436+ update_if_necessary ! ( * tx_type, Some ( new_tx_type. clone( ) ) ) ;
437+ } ,
438+ _ => { } ,
439+ }
440+ }
441+
294442 if updated {
295443 self . latest_update_timestamp = SystemTime :: now ( )
296444 . duration_since ( UNIX_EPOCH )
@@ -351,6 +499,8 @@ pub enum PaymentKind {
351499 txid : Txid ,
352500 /// The confirmation status of this payment.
353501 status : ConfirmationStatus ,
502+ /// The type of the on-chain transaction, if known.
503+ tx_type : Option < TransactionType > ,
354504 } ,
355505 /// A [BOLT 11] payment.
356506 ///
@@ -448,6 +598,7 @@ pub enum PaymentKind {
448598impl_writeable_tlv_based_enum ! ( PaymentKind ,
449599 ( 0 , Onchain ) => {
450600 ( 0 , txid, required) ,
601+ ( 1 , tx_type, option) ,
451602 ( 2 , status, required) ,
452603 } ,
453604 ( 2 , Bolt11 ) => {
@@ -540,6 +691,7 @@ pub(crate) struct PaymentDetailsUpdate {
540691 pub direction : Option < PaymentDirection > ,
541692 pub status : Option < PaymentStatus > ,
542693 pub confirmation_status : Option < ConfirmationStatus > ,
694+ pub tx_type : Option < Option < TransactionType > > ,
543695}
544696
545697impl PaymentDetailsUpdate {
@@ -555,6 +707,7 @@ impl PaymentDetailsUpdate {
555707 direction : None ,
556708 status : None ,
557709 confirmation_status : None ,
710+ tx_type : None ,
558711 }
559712 }
560713}
@@ -570,9 +723,11 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
570723 _ => ( None , None , None ) ,
571724 } ;
572725
573- let confirmation_status = match value. kind {
574- PaymentKind :: Onchain { status, .. } => Some ( status) ,
575- _ => None ,
726+ let ( confirmation_status, tx_type) = match value. kind {
727+ PaymentKind :: Onchain { status, ref tx_type, .. } => {
728+ ( Some ( status) , Some ( tx_type. clone ( ) ) )
729+ } ,
730+ _ => ( None , None ) ,
576731 } ;
577732
578733 let counterparty_skimmed_fee_msat = match value. kind {
@@ -593,6 +748,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
593748 direction : Some ( value. direction ) ,
594749 status : Some ( value. status ) ,
595750 confirmation_status,
751+ tx_type,
596752 }
597753 }
598754}
@@ -761,4 +917,108 @@ mod tests {
761917 }
762918 }
763919 }
920+
921+ #[ test]
922+ fn transaction_type_serialization_roundtrip ( ) {
923+ use lightning:: util:: ser:: Writeable ;
924+
925+ let pubkey = PublicKey :: from_slice ( & [
926+ 2 , 238 , 199 , 36 , 93 , 107 , 125 , 44 , 203 , 48 , 56 , 11 , 251 , 226 , 163 , 100 , 140 , 215 , 169 ,
927+ 66 , 101 , 63 , 90 , 163 , 64 , 237 , 206 , 161 , 242 , 131 , 104 , 102 , 25 ,
928+ ] )
929+ . unwrap ( ) ;
930+ let channel_id = ChannelId ( [ 42u8 ; 32 ] ) ;
931+ let channel = Channel { counterparty_node_id : pubkey, channel_id } ;
932+
933+ // Test all variants
934+ let variants: Vec < TransactionType > = vec ! [
935+ TransactionType :: Funding { channels: vec![ channel. clone( ) ] } ,
936+ TransactionType :: CooperativeClose { counterparty_node_id: pubkey, channel_id } ,
937+ TransactionType :: UnilateralClose { counterparty_node_id: pubkey, channel_id } ,
938+ TransactionType :: AnchorBump { counterparty_node_id: pubkey, channel_id } ,
939+ TransactionType :: Claim { counterparty_node_id: pubkey, channel_id } ,
940+ TransactionType :: Sweep { channels: vec![ channel. clone( ) ] } ,
941+ TransactionType :: Splice { counterparty_node_id: pubkey, channel_id } ,
942+ TransactionType :: OnchainSend ,
943+ // Also test empty channel lists
944+ TransactionType :: Funding { channels: vec![ ] } ,
945+ TransactionType :: Sweep { channels: vec![ ] } ,
946+ ] ;
947+
948+ for variant in variants {
949+ let encoded = variant. encode ( ) ;
950+ let decoded = TransactionType :: read ( & mut & * encoded) . unwrap ( ) ;
951+ assert_eq ! ( variant, decoded) ;
952+ }
953+ }
954+
955+ #[ test]
956+ fn onchain_payment_kind_backward_compat ( ) {
957+ // Simulate the old serialization format for PaymentKind::Onchain (without tx_type).
958+ // The old format only had fields (0, txid) and (2, status).
959+ use bitcoin:: hashes:: Hash ;
960+ use lightning:: util:: ser:: Writeable ;
961+
962+ let txid = bitcoin:: Txid :: from_slice ( & [ 42u8 ; 32 ] ) . unwrap ( ) ;
963+ let status = ConfirmationStatus :: Unconfirmed ;
964+
965+ // Manually create a PaymentKind::Onchain with tx_type = None and verify roundtrip.
966+ let kind = PaymentKind :: Onchain { txid, status, tx_type : None } ;
967+ let encoded = kind. encode ( ) ;
968+ let decoded = PaymentKind :: read ( & mut & * encoded) . unwrap ( ) ;
969+ assert_eq ! ( kind, decoded) ;
970+
971+ // Also test with tx_type = Some
972+ let kind_with_type =
973+ PaymentKind :: Onchain { txid, status, tx_type : Some ( TransactionType :: OnchainSend ) } ;
974+ let encoded_with_type = kind_with_type. encode ( ) ;
975+ let decoded_with_type = PaymentKind :: read ( & mut & * encoded_with_type) . unwrap ( ) ;
976+ assert_eq ! ( kind_with_type, decoded_with_type) ;
977+
978+ // Verify that the encoding without tx_type is shorter (backward compat: old readers
979+ // skip the unknown odd field).
980+ assert ! ( encoded. len( ) < encoded_with_type. len( ) ) ;
981+ }
982+
983+ #[ test]
984+ fn from_ldk_transaction_type ( ) {
985+ let pubkey = PublicKey :: from_slice ( & [
986+ 2 , 238 , 199 , 36 , 93 , 107 , 125 , 44 , 203 , 48 , 56 , 11 , 251 , 226 , 163 , 100 , 140 , 215 , 169 ,
987+ 66 , 101 , 63 , 90 , 163 , 64 , 237 , 206 , 161 , 242 , 131 , 104 , 102 , 25 ,
988+ ] )
989+ . unwrap ( ) ;
990+ let channel_id = ChannelId ( [ 42u8 ; 32 ] ) ;
991+
992+ // Test Funding conversion
993+ let ldk_funding = LdkTransactionType :: Funding { channels : vec ! [ ( pubkey, channel_id) ] } ;
994+ let local_funding = TransactionType :: from ( ldk_funding) ;
995+ assert_eq ! (
996+ local_funding,
997+ TransactionType :: Funding {
998+ channels: vec![ Channel { counterparty_node_id: pubkey, channel_id } ]
999+ }
1000+ ) ;
1001+
1002+ // Test CooperativeClose conversion
1003+ let ldk_coop =
1004+ LdkTransactionType :: CooperativeClose { counterparty_node_id : pubkey, channel_id } ;
1005+ let local_coop = TransactionType :: from ( ldk_coop) ;
1006+ assert_eq ! (
1007+ local_coop,
1008+ TransactionType :: CooperativeClose { counterparty_node_id: pubkey, channel_id }
1009+ ) ;
1010+
1011+ // Test Sweep conversion with empty channels
1012+ let ldk_sweep = LdkTransactionType :: Sweep { channels : vec ! [ ] } ;
1013+ let local_sweep = TransactionType :: from ( ldk_sweep) ;
1014+ assert_eq ! ( local_sweep, TransactionType :: Sweep { channels: vec![ ] } ) ;
1015+
1016+ // Test Splice conversion
1017+ let ldk_splice = LdkTransactionType :: Splice { counterparty_node_id : pubkey, channel_id } ;
1018+ let local_splice = TransactionType :: from ( ldk_splice) ;
1019+ assert_eq ! (
1020+ local_splice,
1021+ TransactionType :: Splice { counterparty_node_id: pubkey, channel_id }
1022+ ) ;
1023+ }
7641024}
0 commit comments