wallet: make utxo order deterministic if CLN_DEV_ENTROPY_SEED set.
What changed, and why it matters
This change only affects Core Lightning when a special developer-only environment variable (CLN_DEV_ENTROPY_SEED) is set. It makes the order in which the wallet picks coins deterministic instead of random, which helps developers reproduce tests. In normal operation the wallet still picks coins randomly, so this commit does not introduce a security vulnerability for regular users.
No action required. Reviewers may want to confirm that CLN_DEV_ENTROPY_SEED cannot be enabled in production builds (developer mode is false), preserving random UTXO selection for live nodes.
Security signals we found
Conditional deterministic UTXO ordering gated on developer mode and CLN_DEV_ENTROPY_SEED
Default path retains existing random UTXO selection
No change to output status, reservation, or spending logic
No input validation, parsing, or cryptographic changes
Evidence from the diff
The commit adds a conditional code path in wallet_find_utxo() and gather_utxos(). If and only if w->ld->developer is true and the CLN_DEV_ENTROPY_SEED environment variable is set, UTXO selection is ordered by prev_out_tx/prev_out_index or sorted after loading. Otherwise the existing ORDER BY RANDOM() behavior is preserved. This is a test/reproducibility aid, not a change to default wallet behavior.
Changed components
wallet/wallet.cwallet/test/run-wallet.cInspect captured patch +71 / −22
diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c
index 05a1a0c..617e967 100644
--- a/wallet/test/run-wallet.c
+++ b/wallet/test/run-wallet.c
@@ -1926,6 +1926,7 @@ int main(int argc, const char *argv[])
ld = tal(tmpctx, struct lightningd);
ld->config = test_config;
ld->hsm_capabilities = NULL;
+ ld->developer = true;
memset(&ld->indexes[WAIT_SUBSYSTEM_HTLCS], 0,
sizeof(ld->indexes[WAIT_SUBSYSTEM_HTLCS]));
diff --git a/wallet/wallet.c b/wallet/wallet.c
index 4de784e..07ffadd 100644
--- a/wallet/wallet.c
+++ b/wallet/wallet.c
@@ -1,6 +1,7 @@
#include "config.h"
#include <bitcoin/script.h>
#include <ccan/array_size/array_size.h>
+#include <ccan/asort/asort.h>
#include <ccan/cast/cast.h>
#include <ccan/mem/mem.h>
#include <ccan/tal/str/str.h>
@@ -8,6 +9,7 @@
#include <common/clock_time.h>
#include <common/memleak.h>
#include <common/onionreply.h>
+#include <common/randbytes.h>
#include <common/trace.h>
#include <db/bindings.h>
#include <db/common.h>
@@ -451,7 +453,23 @@ bool wallet_update_output_status(struct wallet *w,
return changes > 0;
}
-static struct utxo **gather_utxos(const tal_t *ctx, struct db_stmt *stmt STEALS)
+static int cmp_utxo(struct utxo *const *a,
+ struct utxo *const *b,
+ void *unused)
+{
+ int ret = memcmp(&(*a)->outpoint.txid, &(*b)->outpoint.txid,
+ sizeof((*a)->outpoint.txid));
+ if (ret)
+ return ret;
+ if ((*a)->outpoint.n < (*b)->outpoint.n)
+ return -1;
+ else if ((*a)->outpoint.n > (*b)->outpoint.n)
+ return 1;
+ return 0;
+}
+
+static struct utxo **gather_utxos(const tal_t *ctx,
+ struct db_stmt *stmt STEALS)
{
struct utxo **results;
@@ -463,6 +481,10 @@ static struct utxo **gather_utxos(const tal_t *ctx, struct db_stmt *stmt STEALS)
}
tal_free(stmt);
+ /* Make sure these are in order if we're trying to remove entropy */
+ if (randbytes_overridden())
+ asort(results, tal_count(results), cmp_utxo, NULL);
+
return results;
}
@@ -809,27 +831,53 @@ struct utxo *wallet_find_utxo(const tal_t *ctx, struct wallet *w,
struct db_stmt *stmt;
struct utxo *utxo;
- stmt = db_prepare_v2(w->db, SQL("SELECT"
- " prev_out_tx"
- ", prev_out_index"
- ", value"
- ", type"
- ", status"
- ", keyindex"
- ", channel_id"
- ", peer_id"
- ", commitment_point"
- ", option_anchor_outputs"
- ", confirmation_height"
- ", spend_height"
- ", scriptpubkey "
- ", reserved_til"
- ", csv_lock"
- ", is_in_coinbase"
- " FROM outputs"
- " WHERE status = ?"
- " OR (status = ? AND reserved_til <= ?)"
- "ORDER BY RANDOM();"));
+ /* Make sure these are in order if we're trying to remove entropy! */
+ if (w->ld->developer && getenv("CLN_DEV_ENTROPY_SEED")) {
+ stmt = db_prepare_v2(w->db, SQL("SELECT"
+ " prev_out_tx"
+ ", prev_out_index"
+ ", value"
+ ", type"
+ ", status"
+ ", keyindex"
+ ", channel_id"
+ ", peer_id"
+ ", commitment_point"
+ ", option_anchor_outputs"
+ ", confirmation_height"
+ ", spend_height"
+ ", scriptpubkey "
+ ", reserved_til"
+ ", csv_lock"
+ ", is_in_coinbase"
+ " FROM outputs"
+ " WHERE status = ?"
+ " OR (status = ? AND reserved_til <= ?)"
+ "ORDER BY prev_out_tx, prev_out_index;"));
+ } else {
+ stmt = db_prepare_v2(w->db, SQL("SELECT"
+ " prev_out_tx"
+ ", prev_out_index"
+ ", value"
+ ", type"
+ ", status"
+ ", keyindex"
+ ", channel_id"
+ ", peer_id"
+ ", commitment_point"
+ ", option_anchor_outputs"
+ ", confirmation_height"
+ ", spend_height"
+ ", scriptpubkey "
+ ", reserved_til"
+ ", csv_lock"
+ ", is_in_coinbase"
+ " FROM outputs"
+ " WHERE status = ?"
+ " OR (status = ? AND reserved_til <= ?)"
+ "ORDER BY RANDOM();"));
+ }
+
db_bind_int(stmt, output_status_in_db(OUTPUT_STATE_AVAILABLE));
db_bind_int(stmt, output_status_in_db(OUTPUT_STATE_RESERVED));
db_bind_u64(stmt, current_blockheight);
Why this scored 18/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.