Skip to content

Commit 95c6c0b

Browse files
committed
Verify fee claims and persist the grant on register
Wires the claim verifier into register_node. The service now holds a set of trusted issuer keys and a long-lived verification context; when a peer registers, any fee_claim it presented is verified against those keys and the granted policy is persisted onto the peer's SCID record before the response is enqueued, so the policy is in place by the time the SCID is handed out. The feature is inert by default. An empty issuer_pubkeys set short-circuits before any crypto and resolves every peer to the standard policy, byte for byte identical to today. Population of the key set is the operator's job downstream; nothing in-tree sets it. A grant is only ever upserted, never downgraded. An absent or unverifiable claim returns None and leaves an existing record untouched, so a transient miss or a malformed claim can't wipe a live grant; a brand-new record falls back to standard. This deviates from a literal "else standard" on purpose: once a node has been granted zero-fee, a dropped claim on a later re-registration must not silently restore the 2% skim. The verifier borrows the service's context rather than allocating per call, and resolve_claim_policy logs and swallows verification failures rather than failing the registration: a bad claim should cost the node its discount, not its channel. add_intercepted_scid is removed. It only built a standard-policy record, which the new persist path now does directly with the resolved policy, so the old helper had no remaining caller.
1 parent 30a7750 commit 95c6c0b

2 files changed

Lines changed: 69 additions & 25 deletions

File tree

lightning-liquidity/src/lsps4/scid_store.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,6 @@ where L::Target: Logger, KV::Target: KVStoreSync {
184184
Ok(())
185185
}
186186

187-
pub fn add_intercepted_scid(
188-
&self, scid: u64, peer_id: PublicKey,
189-
) -> Result<bool, io::Error> {
190-
let scid = ScidWithPeer::new(scid, peer_id, FeePolicy::Flat(FeeTier::Standard));
191-
self.insert(scid)
192-
}
193-
194187
pub fn get_peer(&self, scid: u64) -> Option<PublicKey> {
195188
use lightning::log_debug;
196189
let result = self.peer_by_scid.read().unwrap().get(&scid).cloned();
@@ -307,7 +300,7 @@ mod tests {
307300
#[test]
308301
fn default_record_resolves_to_standard_policy() {
309302
let store = test_store();
310-
store.add_intercepted_scid(42, test_peer()).unwrap();
303+
store.insert(ScidWithPeer::new(42, test_peer(), FeePolicy::Flat(FeeTier::Standard))).unwrap();
311304

312305
assert_eq!(store.get_policy(&test_peer()), Some(FeePolicy::Flat(FeeTier::Standard)));
313306
}

lightning-liquidity/src/lsps4/service.rs

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ use crate::lsps0::ser::{
1515
JSONRPC_INTERNAL_ERROR_ERROR_CODE, JSONRPC_INTERNAL_ERROR_ERROR_MESSAGE,
1616
LSPS0_CLIENT_REJECTED_ERROR_CODE,
1717
};
18+
use crate::lsps4::claim::verify_claim;
1819
use crate::lsps4::event::LSPS4ServiceEvent;
1920
use crate::lsps4::htlc_store::{HTLCStore, InterceptedHtlc};
2021
use crate::lsps4::fee_policy::{resolve_skim, FeePolicy, FeeTier};
21-
use crate::lsps4::scid_store::ScidStore;
22+
use crate::lsps4::scid_store::{ScidStore, ScidWithPeer};
2223
use crate::message_queue::MessageQueue;
2324
use crate::prelude::hash_map::Entry;
2425
use crate::prelude::{new_hash_map, HashMap};
@@ -34,7 +35,7 @@ use lightning::util::logger::{Level, Logger};
3435
use lightning::util::persist::{KVStore, KVStoreSync};
3536
use lightning_types::payment::PaymentHash;
3637

37-
use bitcoin::secp256k1::PublicKey;
38+
use bitcoin::secp256k1::{PublicKey, Secp256k1, VerifyOnly, XOnlyPublicKey};
3839

3940
use core::ops::Deref;
4041
use core::sync::atomic::{AtomicUsize, Ordering};
@@ -105,6 +106,10 @@ pub struct LSPS4ServiceConfig {
105106
pub cltv_expiry_delta: u32,
106107
/// The proportional fee, in millionths, to skim from forwarded payments.
107108
pub forwarding_fee_proportional_millionths: u64,
109+
/// Issuer keys trusted to sign fee-policy grants. Empty disables the feature: every claim is
110+
/// rejected and every peer resolves to the standard policy. A claim signed by any one of these
111+
/// keys is honoured.
112+
pub issuer_pubkeys: Vec<XOnlyPublicKey>,
108113
}
109114

110115
/// The main object allowing to send and receive LSPS4 messages.
@@ -122,6 +127,8 @@ where
122127
htlc_store: HTLCStore<L, K>,
123128
connected_peers: RwLock<HashSet<PublicKey>>,
124129
config: LSPS4ServiceConfig,
130+
/// Long-lived context for verifying fee-claim signatures during registration.
131+
secp_ctx: Secp256k1<VerifyOnly>,
125132
/// Per-peer timestamped cooldown for liquidity actions (splice or channel open).
126133
/// Prevents 1Hz retry loops from process_pending_htlcs when actions fail.
127134
/// Auto-expires after LIQUIDITY_COOLDOWN_SECS.
@@ -152,6 +159,7 @@ where
152159
config,
153160
logger,
154161
connected_peers: RwLock::new(HashSet::new()),
162+
secp_ctx: Secp256k1::verification_only(),
155163
liquidity_cooldown: RwLock::new(new_hash_map()),
156164
})
157165
}
@@ -598,8 +606,56 @@ where
598606
}
599607
}
600608

609+
/// Resolve any fee-policy grant the registering node presented.
610+
///
611+
/// Returns `None` when the feature is off (no issuer keys configured), when no claim was
612+
/// presented, or when the claim does not verify. A `None` must never downgrade a live grant, so
613+
/// the caller only upserts on `Some`; a new record falls back to the standard policy.
614+
fn resolve_claim_policy(
615+
&self, counterparty: &PublicKey, fee_claim: &Option<String>,
616+
) -> Option<FeePolicy> {
617+
if self.config.issuer_pubkeys.is_empty() {
618+
return None;
619+
}
620+
let fee_claim = fee_claim.as_ref()?;
621+
match verify_claim(&self.secp_ctx, fee_claim, &self.config.issuer_pubkeys, counterparty) {
622+
Ok(policy) => Some(policy),
623+
Err(e) => {
624+
log_error!(
625+
self.logger,
626+
"[LSPS4] Rejected fee claim from {}: {:?}",
627+
counterparty,
628+
e
629+
);
630+
None
631+
},
632+
}
633+
}
634+
635+
/// Persist (upsert) a SCID record carrying the resolved fee policy for a peer.
636+
fn persist_scid_policy(
637+
&self, intercept_scid: u64, peer: &PublicKey, policy: FeePolicy,
638+
) -> Result<(), LightningError> {
639+
self.scid_store
640+
.insert(ScidWithPeer::new(intercept_scid, peer.clone(), policy))
641+
.map(|_| ())
642+
.map_err(|e| {
643+
log_error!(
644+
self.logger,
645+
"[LSPS4] Failed to persist intercept SCID {} for peer {}: {}",
646+
intercept_scid,
647+
peer,
648+
e
649+
);
650+
LightningError {
651+
err: format!("Failed to add intercepted SCID: {}", e),
652+
action: ErrorAction::IgnoreAndLog(Level::Error),
653+
}
654+
})
655+
}
656+
601657
fn handle_register_node_request(
602-
&self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, _params: RegisterNodeRequest,
658+
&self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, params: RegisterNodeRequest,
603659
) -> Result<(), LightningError> {
604660
log_info!(
605661
self.logger,
@@ -608,6 +664,8 @@ where
608664
request_id
609665
);
610666

667+
let granted_policy = self.resolve_claim_policy(counterparty_node_id, &params.fee_claim);
668+
611669
let intercept_scid = match self.scid_store.get_scid(counterparty_node_id) {
612670
Some(intercept_scid) => {
613671
log_info!(
@@ -616,6 +674,11 @@ where
616674
intercept_scid,
617675
counterparty_node_id
618676
);
677+
// Upsert a verified grant onto the existing record. An absent or invalid claim
678+
// leaves the live policy untouched so a transient miss can't wipe a grant.
679+
if let Some(policy) = granted_policy {
680+
self.persist_scid_policy(intercept_scid, counterparty_node_id, policy)?;
681+
}
619682
intercept_scid
620683
},
621684
None => {
@@ -626,20 +689,8 @@ where
626689
intercept_scid,
627690
counterparty_node_id
628691
);
629-
self.scid_store.add_intercepted_scid(intercept_scid, counterparty_node_id.clone())
630-
.map_err(|e| {
631-
log_error!(
632-
self.logger,
633-
"[LSPS4] Failed to persist intercept SCID {} for peer {}: {}",
634-
intercept_scid,
635-
counterparty_node_id,
636-
e
637-
);
638-
LightningError {
639-
err: format!("Failed to add intercepted SCID: {}", e),
640-
action: ErrorAction::IgnoreAndLog(Level::Error),
641-
}
642-
})?;
692+
let policy = granted_policy.unwrap_or(FeePolicy::Flat(FeeTier::Standard));
693+
self.persist_scid_policy(intercept_scid, counterparty_node_id, policy)?;
643694
log_info!(
644695
self.logger,
645696
"[LSPS4] Successfully stored intercept SCID {} for peer {}",

0 commit comments

Comments
 (0)