@@ -92,8 +92,8 @@ use core::ops::Deref;
9292pub ( super ) enum PendingHTLCRouting {
9393 Forward {
9494 onion_packet : msgs:: OnionPacket ,
95- /// The SCID from the onion that we should forward to. This could be a " real" SCID, an
96- /// outbound SCID alias, or a phantom node SCID .
95+ /// The SCID from the onion that we should forward to. This could be a real SCID or a fake one
96+ /// generated using `get_fake_scid` from the scid_utils::fake_scid module .
9797 short_channel_id : u64 , // This should be NonZero<u64> eventually when we bump MSRV
9898 } ,
9999 Receive {
@@ -207,6 +207,24 @@ impl Readable for PaymentId {
207207 Ok ( PaymentId ( buf) )
208208 }
209209}
210+
211+ /// An identifier used to uniquely identify an intercepted HTLC to LDK.
212+ /// (C-not exported) as we just use [u8; 32] directly
213+ #[ derive( Hash , Copy , Clone , PartialEq , Eq , Debug ) ]
214+ pub struct InterceptId ( pub [ u8 ; 32 ] ) ;
215+
216+ impl Writeable for InterceptId {
217+ fn write < W : Writer > ( & self , w : & mut W ) -> Result < ( ) , io:: Error > {
218+ self . 0 . write ( w)
219+ }
220+ }
221+
222+ impl Readable for InterceptId {
223+ fn read < R : Read > ( r : & mut R ) -> Result < Self , DecodeError > {
224+ let buf: [ u8 ; 32 ] = Readable :: read ( r) ?;
225+ Ok ( InterceptId ( buf) )
226+ }
227+ }
210228/// Tracks the inbound corresponding to an outbound HTLC
211229#[ allow( clippy:: derive_hash_xor_eq) ] // Our Hash is faithful to the data, we just don't have SecretKey::hash
212230#[ derive( Clone , PartialEq , Eq ) ]
@@ -666,6 +684,8 @@ pub type SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, M, T, F, L> = ChannelManage
666684// `total_consistency_lock`
667685// |
668686// |__`forward_htlcs`
687+ // | |
688+ // | |__`pending_intercepted_htlcs`
669689// |
670690// |__`pending_inbound_payments`
671691// | |
@@ -751,6 +771,11 @@ pub struct ChannelManager<M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
751771 pub ( super ) forward_htlcs : Mutex < HashMap < u64 , Vec < HTLCForwardInfo > > > ,
752772 #[ cfg( not( test) ) ]
753773 forward_htlcs : Mutex < HashMap < u64 , Vec < HTLCForwardInfo > > > ,
774+ /// Storage for HTLCs that have been intercepted and bubbled up to the user. We hold them here
775+ /// until the user tells us what we should do with them.
776+ ///
777+ /// See `ChannelManager` struct-level documentation for lock order requirements.
778+ pending_intercepted_htlcs : Mutex < HashMap < InterceptId , PendingAddHTLCInfo > > ,
754779
755780 /// Map from payment hash to the payment data and any HTLCs which are to us and can be
756781 /// failed/claimed by the user.
@@ -1566,6 +1591,7 @@ impl<M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelManager<M, T, K, F
15661591 pending_outbound_payments : Mutex :: new ( HashMap :: new ( ) ) ,
15671592 forward_htlcs : Mutex :: new ( HashMap :: new ( ) ) ,
15681593 claimable_htlcs : Mutex :: new ( HashMap :: new ( ) ) ,
1594+ pending_intercepted_htlcs : Mutex :: new ( HashMap :: new ( ) ) ,
15691595 id_to_peer : Mutex :: new ( HashMap :: new ( ) ) ,
15701596 short_to_chan_info : FairRwLock :: new ( HashMap :: new ( ) ) ,
15711597
@@ -2206,8 +2232,11 @@ impl<M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelManager<M, T, K, F
22062232 let forwarding_id_opt = match id_option {
22072233 None => { // unknown_next_peer
22082234 // Note that this is likely a timing oracle for detecting whether an scid is a
2209- // phantom.
2210- if fake_scid:: is_valid_phantom ( & self . fake_scid_rand_bytes , * short_channel_id, & self . genesis_hash ) {
2235+ // phantom or an intercept.
2236+ if ( self . default_configuration . accept_intercept_htlcs &&
2237+ fake_scid:: is_valid_intercept ( & self . fake_scid_rand_bytes , * short_channel_id, & self . genesis_hash ) ) ||
2238+ fake_scid:: is_valid_phantom ( & self . fake_scid_rand_bytes , * short_channel_id, & self . genesis_hash )
2239+ {
22112240 None
22122241 } else {
22132242 break Some ( ( "Don't have available channel for forwarding as requested." , 0x4000 | 10 , None ) ) ;
@@ -3023,6 +3052,102 @@ impl<M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelManager<M, T, K, F
30233052 Ok ( ( ) )
30243053 }
30253054
3055+ /// Attempts to forward an intercepted HTLC over the provided channel id and with the provided
3056+ /// amount to forward. Should only be called in response to an [`HTLCIntercepted`] event.
3057+ ///
3058+ /// Intercepted HTLCs can be useful for Lightning Service Providers (LSPs) to open a just-in-time
3059+ /// channel to a receiving node if the node lacks sufficient inbound liquidity.
3060+ ///
3061+ /// To make use of intercepted HTLCs, set [`UserConfig::accept_intercept_htlcs`] and use
3062+ /// [`ChannelManager::get_intercept_scid`] to generate short channel id(s) to put in the
3063+ /// receiver's invoice route hints. These route hints will signal to LDK to generate an
3064+ /// [`HTLCIntercepted`] event when it receives the forwarded HTLC, and this method or
3065+ /// [`ChannelManager::fail_intercepted_htlc`] MUST be called in response to the event.
3066+ ///
3067+ /// Note that LDK does not enforce fee requirements in `amt_to_forward_msat`, and will not stop
3068+ /// you from forwarding more than you received.
3069+ ///
3070+ /// Errors if the event was not handled in time, in which case the HTLC was automatically failed
3071+ /// backwards.
3072+ ///
3073+ /// [`UserConfig::accept_intercept_htlcs`]: crate::util::config::UserConfig::accept_intercept_htlcs
3074+ /// [`HTLCIntercepted`]: events::Event::HTLCIntercepted
3075+ // TODO: when we move to deciding the best outbound channel at forward time, only take
3076+ // `next_node_id` and not `next_hop_channel_id`
3077+ pub fn forward_intercepted_htlc ( & self , intercept_id : InterceptId , next_hop_channel_id : & [ u8 ; 32 ] , _next_node_id : PublicKey , amt_to_forward_msat : u64 ) -> Result < ( ) , APIError > {
3078+ let _persistence_guard = PersistenceNotifierGuard :: notify_on_drop ( & self . total_consistency_lock , & self . persistence_notifier ) ;
3079+
3080+ let next_hop_scid = match self . channel_state . lock ( ) . unwrap ( ) . by_id . get ( next_hop_channel_id) {
3081+ Some ( chan) => {
3082+ if !chan. is_usable ( ) {
3083+ return Err ( APIError :: APIMisuseError {
3084+ err : format ! ( "Channel with id {:?} not fully established" , next_hop_channel_id)
3085+ } )
3086+ }
3087+ chan. get_short_channel_id ( ) . unwrap_or ( chan. outbound_scid_alias ( ) )
3088+ } ,
3089+ None => return Err ( APIError :: APIMisuseError {
3090+ err : format ! ( "Channel with id {:?} not found" , next_hop_channel_id)
3091+ } )
3092+ } ;
3093+
3094+ let payment = self . pending_intercepted_htlcs . lock ( ) . unwrap ( ) . remove ( & intercept_id)
3095+ . ok_or_else ( || APIError :: APIMisuseError {
3096+ err : format ! ( "Payment with intercept id {:?} not found" , intercept_id. 0 )
3097+ } ) ?;
3098+
3099+ let routing = match payment. forward_info . routing {
3100+ PendingHTLCRouting :: Forward { onion_packet, .. } => {
3101+ PendingHTLCRouting :: Forward { onion_packet, short_channel_id : next_hop_scid }
3102+ } ,
3103+ _ => unreachable ! ( ) // Only `PendingHTLCRouting::Forward`s are intercepted
3104+ } ;
3105+ let pending_htlc_info = PendingHTLCInfo {
3106+ outgoing_amt_msat : amt_to_forward_msat, routing, ..payment. forward_info
3107+ } ;
3108+
3109+ let mut per_source_pending_forward = [ (
3110+ payment. prev_short_channel_id ,
3111+ payment. prev_funding_outpoint ,
3112+ payment. prev_user_channel_id ,
3113+ vec ! [ ( pending_htlc_info, payment. prev_htlc_id) ]
3114+ ) ] ;
3115+ self . forward_htlcs ( & mut per_source_pending_forward) ;
3116+ Ok ( ( ) )
3117+ }
3118+
3119+ /// Fails the intercepted HTLC indicated by intercept_id. Should only be called in response to
3120+ /// an [`HTLCIntercepted`] event. See [`ChannelManager::forward_intercepted_htlc`].
3121+ ///
3122+ /// Errors if the event was not handled in time, in which case the HTLC was automatically failed
3123+ /// backwards.
3124+ ///
3125+ /// [`HTLCIntercepted`]: events::Event::HTLCIntercepted
3126+ pub fn fail_intercepted_htlc ( & self , intercept_id : InterceptId ) -> Result < ( ) , APIError > {
3127+ let _persistence_guard = PersistenceNotifierGuard :: notify_on_drop ( & self . total_consistency_lock , & self . persistence_notifier ) ;
3128+
3129+ let payment = self . pending_intercepted_htlcs . lock ( ) . unwrap ( ) . remove ( & intercept_id)
3130+ . ok_or_else ( || APIError :: APIMisuseError {
3131+ err : format ! ( "Payment with InterceptId {:?} not found" , intercept_id)
3132+ } ) ?;
3133+
3134+ if let PendingHTLCRouting :: Forward { short_channel_id, .. } = payment. forward_info . routing {
3135+ let htlc_source = HTLCSource :: PreviousHopData ( HTLCPreviousHopData {
3136+ short_channel_id : payment. prev_short_channel_id ,
3137+ outpoint : payment. prev_funding_outpoint ,
3138+ htlc_id : payment. prev_htlc_id ,
3139+ incoming_packet_shared_secret : payment. forward_info . incoming_shared_secret ,
3140+ phantom_shared_secret : None ,
3141+ } ) ;
3142+
3143+ let failure_reason = HTLCFailReason :: Reason { failure_code : 0x4000 | 10 , data : Vec :: new ( ) } ;
3144+ let destination = HTLCDestination :: UnknownNextHop { requested_forward_scid : short_channel_id } ;
3145+ self . fail_htlc_backwards_internal ( htlc_source, & payment. forward_info . payment_hash , failure_reason, destination) ;
3146+ } else { unreachable ! ( ) } // Only `PendingHTLCRouting::Forward`s are intercepted
3147+
3148+ Ok ( ( ) )
3149+ }
3150+
30263151 /// Processes HTLCs which are pending waiting on random forward delay.
30273152 ///
30283153 /// Should only really ever be called in response to a PendingHTLCsForwardable event.
@@ -5067,28 +5192,82 @@ impl<M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelManager<M, T, K, F
50675192 fn forward_htlcs ( & self , per_source_pending_forwards : & mut [ ( u64 , OutPoint , u128 , Vec < ( PendingHTLCInfo , u64 ) > ) ] ) {
50685193 for & mut ( prev_short_channel_id, prev_funding_outpoint, prev_user_channel_id, ref mut pending_forwards) in per_source_pending_forwards {
50695194 let mut forward_event = None ;
5195+ let mut new_intercept_events = Vec :: new ( ) ;
5196+ let mut failed_intercept_forwards = Vec :: new ( ) ;
50705197 if !pending_forwards. is_empty ( ) {
5071- let mut forward_htlcs = self . forward_htlcs . lock ( ) . unwrap ( ) ;
5072- if forward_htlcs. is_empty ( ) {
5073- forward_event = Some ( Duration :: from_millis ( MIN_HTLC_RELAY_HOLDING_CELL_MILLIS ) )
5074- }
50755198 for ( forward_info, prev_htlc_id) in pending_forwards. drain ( ..) {
5076- match forward_htlcs. entry ( match forward_info. routing {
5077- PendingHTLCRouting :: Forward { short_channel_id, .. } => short_channel_id,
5078- PendingHTLCRouting :: Receive { .. } => 0 ,
5079- PendingHTLCRouting :: ReceiveKeysend { .. } => 0 ,
5080- } ) {
5199+ let scid = match forward_info. routing {
5200+ PendingHTLCRouting :: Forward { short_channel_id, .. } => short_channel_id,
5201+ PendingHTLCRouting :: Receive { .. } => 0 ,
5202+ PendingHTLCRouting :: ReceiveKeysend { .. } => 0 ,
5203+ } ;
5204+ // Pull this now to avoid introducing a lock order with `forward_htlcs`.
5205+ let is_our_scid = self . short_to_chan_info . read ( ) . unwrap ( ) . contains_key ( & scid) ;
5206+
5207+ let mut forward_htlcs = self . forward_htlcs . lock ( ) . unwrap ( ) ;
5208+ let forward_htlcs_empty = forward_htlcs. is_empty ( ) ;
5209+ match forward_htlcs. entry ( scid) {
50815210 hash_map:: Entry :: Occupied ( mut entry) => {
50825211 entry. get_mut ( ) . push ( HTLCForwardInfo :: AddHTLC ( PendingAddHTLCInfo {
50835212 prev_short_channel_id, prev_funding_outpoint, prev_htlc_id, prev_user_channel_id, forward_info } ) ) ;
50845213 } ,
50855214 hash_map:: Entry :: Vacant ( entry) => {
5086- entry. insert ( vec ! ( HTLCForwardInfo :: AddHTLC ( PendingAddHTLCInfo {
5087- prev_short_channel_id, prev_funding_outpoint, prev_htlc_id, prev_user_channel_id, forward_info } ) ) ) ;
5215+ if !is_our_scid && forward_info. incoming_amt_msat . is_some ( ) &&
5216+ fake_scid:: is_valid_intercept ( & self . fake_scid_rand_bytes , scid, & self . genesis_hash )
5217+ {
5218+ let intercept_id = InterceptId ( Sha256 :: hash ( & forward_info. incoming_shared_secret ) . into_inner ( ) ) ;
5219+ let mut pending_intercepts = self . pending_intercepted_htlcs . lock ( ) . unwrap ( ) ;
5220+ match pending_intercepts. entry ( intercept_id) {
5221+ hash_map:: Entry :: Vacant ( entry) => {
5222+ new_intercept_events. push ( events:: Event :: HTLCIntercepted {
5223+ requested_next_hop_scid : scid,
5224+ payment_hash : forward_info. payment_hash ,
5225+ inbound_amount_msat : forward_info. incoming_amt_msat . unwrap ( ) ,
5226+ expected_outbound_amount_msat : forward_info. outgoing_amt_msat ,
5227+ intercept_id
5228+ } ) ;
5229+ entry. insert ( PendingAddHTLCInfo {
5230+ prev_short_channel_id, prev_funding_outpoint, prev_htlc_id, prev_user_channel_id, forward_info } ) ;
5231+ } ,
5232+ hash_map:: Entry :: Occupied ( _) => {
5233+ log_info ! ( self . logger, "Failed to forward incoming HTLC: detected duplicate intercepted payment over short channel id {}" , scid) ;
5234+ let htlc_source = HTLCSource :: PreviousHopData ( HTLCPreviousHopData {
5235+ short_channel_id : prev_short_channel_id,
5236+ outpoint : prev_funding_outpoint,
5237+ htlc_id : prev_htlc_id,
5238+ incoming_packet_shared_secret : forward_info. incoming_shared_secret ,
5239+ phantom_shared_secret : None ,
5240+ } ) ;
5241+
5242+ failed_intercept_forwards. push ( ( htlc_source, forward_info. payment_hash ,
5243+ HTLCFailReason :: Reason { failure_code : 0x4000 | 10 , data : Vec :: new ( ) } ,
5244+ HTLCDestination :: InvalidForward { requested_forward_scid : scid } ,
5245+ ) ) ;
5246+ }
5247+ }
5248+ } else {
5249+ // We don't want to generate a PendingHTLCsForwardable event if only intercepted
5250+ // payments are being processed.
5251+ if forward_htlcs_empty {
5252+ forward_event = Some ( Duration :: from_millis ( MIN_HTLC_RELAY_HOLDING_CELL_MILLIS ) ) ;
5253+ }
5254+ entry. insert ( vec ! ( HTLCForwardInfo :: AddHTLC ( PendingAddHTLCInfo {
5255+ prev_short_channel_id, prev_funding_outpoint, prev_htlc_id, prev_user_channel_id, forward_info } ) ) ) ;
5256+ }
50885257 }
50895258 }
50905259 }
50915260 }
5261+
5262+ for ( htlc_source, payment_hash, failure_reason, destination) in failed_intercept_forwards. drain ( ..) {
5263+ self . fail_htlc_backwards_internal ( htlc_source, & payment_hash, failure_reason, destination) ;
5264+ }
5265+
5266+ if !new_intercept_events. is_empty ( ) {
5267+ let mut events = self . pending_events . lock ( ) . unwrap ( ) ;
5268+ events. append ( & mut new_intercept_events) ;
5269+ }
5270+
50925271 match forward_event {
50935272 Some ( time) => {
50945273 let mut pending_events = self . pending_events . lock ( ) . unwrap ( ) ;
@@ -5690,6 +5869,23 @@ impl<M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> ChannelManager<M, T, K, F
56905869 }
56915870 }
56925871
5872+ /// Gets a fake short channel id for use in receiving intercepted payments. These fake scids are
5873+ /// used when constructing the route hints for HTLCs intended to be intercepted. See
5874+ /// [`ChannelManager::forward_intercepted_htlc`].
5875+ ///
5876+ /// Note that this method is not guaranteed to return unique values, you may need to call it a few
5877+ /// times to get a unique scid.
5878+ pub fn get_intercept_scid ( & self ) -> u64 {
5879+ let best_block_height = self . best_block . read ( ) . unwrap ( ) . height ( ) ;
5880+ let short_to_chan_info = self . short_to_chan_info . read ( ) . unwrap ( ) ;
5881+ loop {
5882+ let scid_candidate = fake_scid:: Namespace :: Intercept . get_fake_scid ( best_block_height, & self . genesis_hash , & self . fake_scid_rand_bytes , & self . keys_manager ) ;
5883+ // Ensure the generated scid doesn't conflict with a real channel.
5884+ if short_to_chan_info. contains_key ( & scid_candidate) { continue }
5885+ return scid_candidate
5886+ }
5887+ }
5888+
56935889 /// Gets inflight HTLC information by processing pending outbound payments that are in
56945890 /// our channels. May be used during pathfinding to account for in-use channel liquidity.
56955891 pub fn compute_inflight_htlcs ( & self ) -> InFlightHtlcs {
@@ -6073,7 +6269,6 @@ where
60736269 if height >= htlc. cltv_expiry - HTLC_FAIL_BACK_BUFFER {
60746270 let mut htlc_msat_height_data = byte_utils:: be64_to_array ( htlc. value ) . to_vec ( ) ;
60756271 htlc_msat_height_data. extend_from_slice ( & byte_utils:: be32_to_array ( height) ) ;
6076-
60776272 timed_out_htlcs. push ( ( HTLCSource :: PreviousHopData ( htlc. prev_hop . clone ( ) ) , payment_hash. clone ( ) , HTLCFailReason :: Reason {
60786273 failure_code : 0x4000 | 15 ,
60796274 data : htlc_msat_height_data
@@ -6083,6 +6278,29 @@ where
60836278 } ) ;
60846279 !htlcs. is_empty ( ) // Only retain this entry if htlcs has at least one entry.
60856280 } ) ;
6281+
6282+ let mut intercepted_htlcs = self . pending_intercepted_htlcs . lock ( ) . unwrap ( ) ;
6283+ intercepted_htlcs. retain ( |_, htlc| {
6284+ if height >= htlc. forward_info . outgoing_cltv_value - HTLC_FAIL_BACK_BUFFER {
6285+ let prev_hop_data = HTLCSource :: PreviousHopData ( HTLCPreviousHopData {
6286+ short_channel_id : htlc. prev_short_channel_id ,
6287+ htlc_id : htlc. prev_htlc_id ,
6288+ incoming_packet_shared_secret : htlc. forward_info . incoming_shared_secret ,
6289+ phantom_shared_secret : None ,
6290+ outpoint : htlc. prev_funding_outpoint ,
6291+ } ) ;
6292+
6293+ let requested_forward_scid /* intercept scid */ = match htlc. forward_info . routing {
6294+ PendingHTLCRouting :: Forward { short_channel_id, .. } => short_channel_id,
6295+ _ => unreachable ! ( ) ,
6296+ } ;
6297+ timed_out_htlcs. push ( ( prev_hop_data, htlc. forward_info . payment_hash ,
6298+ HTLCFailReason :: Reason { failure_code : 0x2000 | 2 , data : Vec :: new ( ) } ,
6299+ HTLCDestination :: InvalidForward { requested_forward_scid } ) ) ;
6300+ log_trace ! ( self . logger, "Timing out intercepted HTLC with requested forward scid {}" , requested_forward_scid) ;
6301+ false
6302+ } else { true }
6303+ } ) ;
60866304 }
60876305
60886306 self . handle_init_event_channel_failures ( failed_channels) ;
@@ -6991,8 +7209,15 @@ impl<M: Deref, T: Deref, K: Deref, F: Deref, L: Deref> Writeable for ChannelMana
69917209 _ => { } ,
69927210 }
69937211 }
7212+
7213+ let mut pending_intercepted_htlcs = None ;
7214+ let our_pending_intercepts = self . pending_intercepted_htlcs . lock ( ) . unwrap ( ) ;
7215+ if our_pending_intercepts. len ( ) != 0 {
7216+ pending_intercepted_htlcs = Some ( our_pending_intercepts) ;
7217+ }
69947218 write_tlv_fields ! ( writer, {
69957219 ( 1 , pending_outbound_payments_no_retry, required) ,
7220+ ( 2 , pending_intercepted_htlcs, option) ,
69967221 ( 3 , pending_outbound_payments, required) ,
69977222 ( 5 , self . our_network_pubkey, required) ,
69987223 ( 7 , self . fake_scid_rand_bytes, required) ,
@@ -7306,12 +7531,14 @@ impl<'a, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
73067531 // pending_outbound_payments_no_retry is for compatibility with 0.0.101 clients.
73077532 let mut pending_outbound_payments_no_retry: Option < HashMap < PaymentId , HashSet < [ u8 ; 32 ] > > > = None ;
73087533 let mut pending_outbound_payments = None ;
7534+ let mut pending_intercepted_htlcs: Option < HashMap < InterceptId , PendingAddHTLCInfo > > = Some ( HashMap :: new ( ) ) ;
73097535 let mut received_network_pubkey: Option < PublicKey > = None ;
73107536 let mut fake_scid_rand_bytes: Option < [ u8 ; 32 ] > = None ;
73117537 let mut probing_cookie_secret: Option < [ u8 ; 32 ] > = None ;
73127538 let mut claimable_htlc_purposes = None ;
73137539 read_tlv_fields ! ( reader, {
73147540 ( 1 , pending_outbound_payments_no_retry, option) ,
7541+ ( 2 , pending_intercepted_htlcs, option) ,
73157542 ( 3 , pending_outbound_payments, option) ,
73167543 ( 5 , received_network_pubkey, option) ,
73177544 ( 7 , fake_scid_rand_bytes, option) ,
@@ -7534,6 +7761,7 @@ impl<'a, M: Deref, T: Deref, K: Deref, F: Deref, L: Deref>
75347761 inbound_payment_key : expanded_inbound_key,
75357762 pending_inbound_payments : Mutex :: new ( pending_inbound_payments) ,
75367763 pending_outbound_payments : Mutex :: new ( pending_outbound_payments. unwrap ( ) ) ,
7764+ pending_intercepted_htlcs : Mutex :: new ( pending_intercepted_htlcs. unwrap ( ) ) ,
75377765
75387766 forward_htlcs : Mutex :: new ( forward_htlcs) ,
75397767 claimable_htlcs : Mutex :: new ( claimable_htlcs) ,
0 commit comments