connectd, gossipd, pay, bcli: use timemono when solely measuring duration for timeouts.
What changed, and why it matters
This commit swaps wall-clock timestamps for monotonic timestamps when measuring durations and timeouts in several parts of Core Lightning. Monotonic time only moves forward at a steady rate and is not affected by system clock changes or developer time overrides. The change makes timeout logic more reliable and prevents accidental misbehavior during testing, but it is a hardening/refactoring patch rather than a fix for an active, exploitable vulnerability.
Treat as a routine hardening/refactoring change. Reviewers should verify that no remaining duration/timeout comparisons in the same code paths still mix timeabs and timemono types, and that the timemono helper functions used (timemono_before, timemono_after, timemono_add, timemono_sub, timemono_between) behave equivalently to their timeabs counterparts for non-negative durations. No urgent security response is indicated.
Security signals we found
Use of monotonic clock for timeout/duration measurements reduces risk of premature or delayed timeouts due to wall-clock jumps
Avoids developer time override affecting production timeout behavior
Touches HTLC forwarding grace-period and channel feerate timeout comparisons
Touches payment deadline and retry logic
No explicit vulnerability, CVE, or security bug is described in the commit
Evidence from the diff
The patch replaces struct timeabs / time_now() with struct timemono / time_mono() in timeout/duration-only contexts across connectd, gossipd, lightningd, and plugins (bcli, keysend, pay). Affected durations include peer ping/keepalive timeouts, gossip store compaction timing, channel feerate/grace-period windows, HTLC forwarding grace periods, bitcoin-cli request timing, and payment retry deadlines. The commit message explicitly states the goal is immunity to clock changes and avoidance of developer time overrides. No logic bug or memory safety flaw is corrected; it is a robustness improvement.
Changed components
connectd (peer keepalive/ping timeout tracking)gossipd (gossip_store compaction timing)lightningd/pay.c (injectpaymentonion old-range grace period)lightningd/peer_control.c (channel feerate/grace-period setup)lightningd/peer_htlcs.c (HTLC forwarding grace-period checks)plugins/bcli.c (bitcoin-cli request duration)plugins/keysend.c (keysend payment deadline)plugins/libplugin-pay.c/h (payment retry deadline)plugins/pay.c (pay command deadline)Inspect captured patch +20 / −20
diff --git a/connectd/connectd.c b/connectd/connectd.c
index 1b19b393..ffe93821 100644
--- a/connectd/connectd.c
+++ b/connectd/connectd.c
@@ -131,7 +131,7 @@ static struct peer *new_peer(struct daemon *daemon,
peer->draining_state = NOT_DRAINING;
peer->peer_in_lastmsg = -1;
peer->peer_outq = msg_queue_new(peer, false);
- peer->last_recv_time = time_now();
+ peer->last_recv_time = time_mono();
peer->is_websocket = is_websocket;
peer->dev_writes_enabled = NULL;
peer->dev_read_enabled = true;
diff --git a/connectd/connectd.h b/connectd/connectd.h
index fd750963..cae8b149 100644
--- a/connectd/connectd.h
+++ b/connectd/connectd.h
@@ -105,7 +105,7 @@ struct peer {
struct oneshot *ping_timer;
/* Last time we received traffic */
- struct timeabs last_recv_time;
+ struct timemono last_recv_time;
/* How long have we been ignoring peer input? */
struct timemono peer_in_lasttime;
diff --git a/connectd/multiplex.c b/connectd/multiplex.c
index a06a2107..5c6fc4fe 100644
--- a/connectd/multiplex.c
+++ b/connectd/multiplex.c
@@ -634,8 +634,8 @@ static void send_ping(struct peer *peer)
/* If it's still sending us traffic, maybe ping reply is backed up?
* That's OK, ping is just to make sure it's still alive, and clearly
* it is. */
- if (time_before(peer->last_recv_time,
- timeabs_sub(time_now(), time_from_sec(60)))) {
+ if (timemono_before(peer->last_recv_time,
+ timemono_sub(time_mono(), time_from_sec(60)))) {
/* Already have a ping in flight? */
if (peer->expecting_pong != PONG_UNEXPECTED) {
status_peer_debug(&peer->id, "Last ping unreturned: hanging up");
@@ -1258,7 +1258,7 @@ static struct io_plan *read_body_from_peer_done(struct io_conn *peer_conn,
}
/* We got something! */
- peer->last_recv_time = time_now();
+ peer->last_recv_time = time_mono();
/* Don't process packets while we're closing */
if (peer->draining_state != NOT_DRAINING)
diff --git a/gossipd/gossip_store.c b/gossipd/gossip_store.c
index 21436251..c68e2454 100644
--- a/gossipd/gossip_store.c
+++ b/gossipd/gossip_store.c
@@ -194,7 +194,7 @@ static int gossip_store_compact(struct daemon *daemon,
struct gossip_hdr hdr;
u8 oldversion, version = GOSSIP_STORE_VER;
struct stat st;
- struct timeabs start = time_now();
+ struct timemono start = time_mono();
const char *bad;
*populated = false;
@@ -360,7 +360,7 @@ rename_new:
}
status_debug("Store compact time: %"PRIu64" msec",
- time_to_msec(time_between(time_now(), start)));
+ time_to_msec(timemono_between(time_mono(), start)));
status_debug("gossip_store: Read %zu/%zu/%zu/%zu cannounce/cupdate/nannounce/delete from store in %"PRIu64" bytes, now %"PRIu64" bytes (populated=%s)",
cannounces, cupdates, nannounces, deleted,
old_len, *total_len,
diff --git a/lightningd/channel.h b/lightningd/channel.h
index 3606d0f9..1281c61e 100644
--- a/lightningd/channel.h
+++ b/lightningd/channel.h
@@ -295,7 +295,7 @@ struct channel {
u32 feerate_base, feerate_ppm;
/* But allow these feerates/htlcs up until this time. */
- struct timeabs old_feerate_timeout;
+ struct timemono old_feerate_timeout;
u32 old_feerate_base, old_feerate_ppm;
struct amount_msat old_htlc_minimum_msat, old_htlc_maximum_msat;
diff --git a/lightningd/pay.c b/lightningd/pay.c
index b38b8e1e..068e142e 100644
--- a/lightningd/pay.c
+++ b/lightningd/pay.c
@@ -2018,7 +2018,7 @@ static struct command_result *json_injectpaymentonion(struct command *cmd,
if (amount_msat_greater(*msat, next->htlc_maximum_msat)
|| amount_msat_less(*msat, next->htlc_minimum_msat)) {
/* Are we in old-range grace-period? */
- if (!time_before(time_now(), next->old_feerate_timeout)
+ if (!timemono_before(time_mono(), next->old_feerate_timeout)
|| amount_msat_less(*msat, next->old_htlc_minimum_msat)
|| amount_msat_greater(*msat, next->old_htlc_maximum_msat)) {
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c
index f5b064ca..4b44384d 100644
--- a/lightningd/peer_control.c
+++ b/lightningd/peer_control.c
@@ -3207,7 +3207,7 @@ static void set_channel_config(struct command *cmd, struct channel *channel,
|| (htlc_max
&& amount_msat_less(*htlc_max, channel->htlc_maximum_msat))) {
channel->old_feerate_timeout
- = timeabs_add(time_now(), time_from_sec(delaysecs));
+ = timemono_add(time_mono(), time_from_sec(delaysecs));
channel->old_feerate_base = channel->feerate_base;
channel->old_feerate_ppm = channel->feerate_ppm;
channel->old_htlc_minimum_msat = channel->htlc_minimum_msat;
diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c
index 7c818782..548a15c8 100644
--- a/lightningd/peer_htlcs.c
+++ b/lightningd/peer_htlcs.c
@@ -848,7 +848,7 @@ static void forward_htlc(struct htlc_in *hin,
* - If it creates a new `channel_update` with updated channel parameters:
* - SHOULD keep accepting the previous channel parameters for 10 minutes
*/
- if (!time_before(time_now(), next->old_feerate_timeout)
+ if (!timemono_before(time_mono(), next->old_feerate_timeout)
|| !check_fwd_amount(hin, amt_to_forward, hin->msat,
next->old_feerate_base,
next->old_feerate_ppm)) {
@@ -864,7 +864,7 @@ static void forward_htlc(struct htlc_in *hin,
if (amount_msat_greater(amt_to_forward, next->htlc_maximum_msat)
|| amount_msat_less(amt_to_forward, next->htlc_minimum_msat)) {
/* Are we in old-range grace-period? */
- if (!time_before(time_now(), next->old_feerate_timeout)
+ if (!timemono_before(time_mono(), next->old_feerate_timeout)
|| amount_msat_less(amt_to_forward, next->old_htlc_minimum_msat)
|| amount_msat_greater(amt_to_forward, next->old_htlc_maximum_msat)) {
failmsg = towire_temporary_channel_failure(tmpctx,
diff --git a/plugins/bcli.c b/plugins/bcli.c
index 7646b348..faa155c7 100644
--- a/plugins/bcli.c
+++ b/plugins/bcli.c
@@ -77,7 +77,7 @@ struct bitcoin_cli {
pid_t pid;
const char **args;
const char **stdinargs;
- struct timeabs start;
+ struct timemono start;
enum bitcoind_prio prio;
char *output;
size_t output_bytes;
@@ -260,7 +260,7 @@ static void bcli_finished(struct io_conn *conn UNUSED, struct bitcoin_cli *bcli)
int ret, status;
struct command_result *res;
enum bitcoind_prio prio = bcli->prio;
- u64 msec = time_to_msec(time_between(time_now(), bcli->start));
+ u64 msec = time_to_msec(timemono_between(time_mono(), bcli->start));
/* If it took over 10 seconds, that's rather strange. */
if (msec > 10000)
@@ -336,7 +336,7 @@ static void next_bcli(enum bitcoind_prio prio)
}
close(in);
- bcli->start = time_now();
+ bcli->start = time_mono();
bitcoind->num_requests[prio]++;
diff --git a/plugins/keysend.c b/plugins/keysend.c
index 2e1ba297..30de252e 100644
--- a/plugins/keysend.c
+++ b/plugins/keysend.c
@@ -239,7 +239,7 @@ static struct command_result *json_keysend(struct command *cmd, const char *buf,
p->invstring_used = true;
p->why = "Initial attempt";
p->constraints.cltv_budget = *maxdelay;
- p->deadline = timeabs_add(time_now(), time_from_sec(*retryfor));
+ p->deadline = timemono_add(time_mono(), time_from_sec(*retryfor));
p->getroute->riskfactorppm = 10000000;
if (node_id_eq(&my_id, p->route_destination)) {
diff --git a/plugins/libplugin-pay.c b/plugins/libplugin-pay.c
index 919ca7de..40814352 100644
--- a/plugins/libplugin-pay.c
+++ b/plugins/libplugin-pay.c
@@ -2526,12 +2526,12 @@ static struct command_result *retry_step_cb(struct retry_mod_data *rd,
{
struct payment *subpayment, *root = payment_root(p);
struct retry_mod_data *rdata = payment_mod_retry_get_data(p);
- struct timeabs now = time_now();
+ struct timemono now = time_mono();
if (p->step != PAYMENT_STEP_FAILED)
return payment_continue(p);
- if (time_after(now, p->deadline)) {
+ if (timemono_after(now, p->deadline)) {
paymod_log(
p, LOG_INFORM,
"Payment deadline expired, not retrying (partial-)payment "
diff --git a/plugins/libplugin-pay.h b/plugins/libplugin-pay.h
index 1498f0fb..50bd0000 100644
--- a/plugins/libplugin-pay.h
+++ b/plugins/libplugin-pay.h
@@ -200,7 +200,7 @@ struct payment {
u32 start_block;
struct timeabs start_time, end_time;
- struct timeabs deadline;
+ struct timemono deadline;
/* Constraints the state machine and modifiers needs to maintain. */
struct payment_constraints constraints;
diff --git a/plugins/pay.c b/plugins/pay.c
index acb532c3..4f44b5e3 100644
--- a/plugins/pay.c
+++ b/plugins/pay.c
@@ -1471,7 +1471,7 @@ static struct command_result *json_pay(struct command *cmd,
p->why = "Initial attempt";
p->constraints.cltv_budget = *maxdelay;
tal_free(maxdelay);
- p->deadline = timeabs_add(time_now(), time_from_sec(*retryfor));
+ p->deadline = timemono_add(time_mono(), time_from_sec(*retryfor));
tal_free(retryfor);
p->getroute->riskfactorppm = *riskfactor_millionths;
tal_free(riskfactor_millionths);
Why this scored 29/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.