Skip to content

Commit d886fba

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 6ab4347 commit d886fba

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};
@@ -89,6 +90,10 @@ pub struct LSPS4ServiceConfig {
8990
pub cltv_expiry_delta: u32,
9091
/// The proportional fee, in millionths, to skim from forwarded payments.
9192
pub forwarding_fee_proportional_millionths: u64,
93+
/// Issuer keys trusted to sign fee-policy grants. Empty disables the feature: every claim is
94+
/// rejected and every peer resolves to the standard policy. A claim signed by any one of these
95+
/// keys is honoured.
96+
pub issuer_pubkeys: Vec<XOnlyPublicKey>,
9297
}
9398

9499
/// The main object allowing to send and receive LSPS4 messages.
@@ -106,6 +111,8 @@ where
106111
htlc_store: HTLCStore<L, K>,
107112
connected_peers: RwLock<HashSet<PublicKey>>,
108113
config: LSPS4ServiceConfig,
114+
/// Long-lived context for verifying fee-claim signatures during registration.
115+
secp_ctx: Secp256k1<VerifyOnly>,
109116
}
110117

111118
impl<CM: Deref + Clone, K: Deref + Clone, L: Deref + Clone> LSPS4ServiceHandler<CM, K, L>
@@ -132,6 +139,7 @@ where
132139
config,
133140
logger,
134141
connected_peers: RwLock::new(HashSet::new()),
142+
secp_ctx: Secp256k1::verification_only(),
135143
})
136144
}
137145

@@ -305,8 +313,56 @@ where
305313
self.connected_peers.write().unwrap().remove(counterparty_node_id);
306314
}
307315

316+
/// Resolve any fee-policy grant the registering node presented.
317+
///
318+
/// Returns `None` when the feature is off (no issuer keys configured), when no claim was
319+
/// presented, or when the claim does not verify. A `None` must never downgrade a live grant, so
320+
/// the caller only upserts on `Some`; a new record falls back to the standard policy.
321+
fn resolve_claim_policy(
322+
&self, counterparty: &PublicKey, fee_claim: &Option<String>,
323+
) -> Option<FeePolicy> {
324+
if self.config.issuer_pubkeys.is_empty() {
325+
return None;
326+
}
327+
let fee_claim = fee_claim.as_ref()?;
328+
match verify_claim(&self.secp_ctx, fee_claim, &self.config.issuer_pubkeys, counterparty) {
329+
Ok(policy) => Some(policy),
330+
Err(e) => {
331+
log_error!(
332+
self.logger,
333+
"[LSPS4] Rejected fee claim from {}: {:?}",
334+
counterparty,
335+
e
336+
);
337+
None
338+
},
339+
}
340+
}
341+
342+
/// Persist (upsert) a SCID record carrying the resolved fee policy for a peer.
343+
fn persist_scid_policy(
344+
&self, intercept_scid: u64, peer: &PublicKey, policy: FeePolicy,
345+
) -> Result<(), LightningError> {
346+
self.scid_store
347+
.insert(ScidWithPeer::new(intercept_scid, peer.clone(), policy))
348+
.map(|_| ())
349+
.map_err(|e| {
350+
log_error!(
351+
self.logger,
352+
"[LSPS4] Failed to persist intercept SCID {} for peer {}: {}",
353+
intercept_scid,
354+
peer,
355+
e
356+
);
357+
LightningError {
358+
err: format!("Failed to add intercepted SCID: {}", e),
359+
action: ErrorAction::IgnoreAndLog(Level::Error),
360+
}
361+
})
362+
}
363+
308364
fn handle_register_node_request(
309-
&self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, _params: RegisterNodeRequest,
365+
&self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, params: RegisterNodeRequest,
310366
) -> Result<(), LightningError> {
311367
let fn_start = Instant::now();
312368
log_info!(
@@ -317,6 +373,8 @@ where
317373
);
318374

319375
let step_start = Instant::now();
376+
let granted_policy = self.resolve_claim_policy(counterparty_node_id, &params.fee_claim);
377+
320378
let intercept_scid = match self.scid_store.get_scid(counterparty_node_id) {
321379
Some(intercept_scid) => {
322380
log_info!(
@@ -326,6 +384,11 @@ where
326384
intercept_scid,
327385
counterparty_node_id
328386
);
387+
// Upsert a verified grant onto the existing record. An absent or invalid claim
388+
// leaves the live policy untouched so a transient miss can't wipe a grant.
389+
if let Some(policy) = granted_policy {
390+
self.persist_scid_policy(intercept_scid, counterparty_node_id, policy)?;
391+
}
329392
intercept_scid
330393
},
331394
None => {
@@ -344,20 +407,8 @@ where
344407
counterparty_node_id
345408
);
346409
let store_start = Instant::now();
347-
self.scid_store.add_intercepted_scid(intercept_scid, counterparty_node_id.clone())
348-
.map_err(|e| {
349-
log_error!(
350-
self.logger,
351-
"[LSPS4] Failed to persist intercept SCID {} for peer {}: {}",
352-
intercept_scid,
353-
counterparty_node_id,
354-
e
355-
);
356-
LightningError {
357-
err: format!("Failed to add intercepted SCID: {}", e),
358-
action: ErrorAction::IgnoreAndLog(Level::Error),
359-
}
360-
})?;
410+
let policy = granted_policy.unwrap_or(FeePolicy::Flat(FeeTier::Standard));
411+
self.persist_scid_policy(intercept_scid, counterparty_node_id, policy)?;
361412
log_info!(
362413
self.logger,
363414
"TIMING: [LSPS4] handle_register_node_request scid_store.add_intercepted_scid() took {}ms - Successfully stored intercept SCID {} for peer {}",

0 commit comments

Comments
 (0)