fuzz-tests: Add a test for `calculate_our_funding()`
What changed, and why it matters
This commit adds a new automated fuzz test for an existing function called calculate_our_funding(), which decides how much bitcoin a Core Lightning node contributes when opening a payment channel. The commit only creates a test file and does not change the actual funding logic. It includes built-in checks that would crash the test if the function ever returns results violating simple rules, such as contributing more than the allowed channel size or more than available funds. There is no indication this commit fixes a security bug or changes production code.
No security action required. Treat as normal test/QA improvement. If running the fuzzer, monitor for crashes that would indicate invariant violations in calculate_our_funding().
Security signals we found
No production code changes
New fuzz test only
Post-condition assertions could detect future bugs in calculate_our_funding()
No changelog security claim
No CVE or advisory references in commit
Evidence from the diff
The diff adds tests/fuzz/fuzz-funder-policy.c, a libFuzzer harness that deserializes random bytes into a struct test_case, calls calculate_our_funding() from plugins/funder_policy.c, and asserts several post-conditions: their_funds + our_funds <= max_channel_size, per_channel_min <= our_funds <= per_channel_max (unless zero), and our_funds <= available_funds - reserve_tank. The test uses bounded modular reduction to keep amount_sat values in range and swaps min/max pairs when inverted. No production code is modified.
Changed components
tests/fuzz/fuzz-funder-policy.cInspect captured patch +167 / −0
diff --git a/tests/fuzz/fuzz-funder-policy.c b/tests/fuzz/fuzz-funder-policy.c
new file mode 100644
index 00000000..a3f80aaa
--- /dev/null
+++ b/tests/fuzz/fuzz-funder-policy.c
@@ -0,0 +1,167 @@
+#include "config.h"
+#include <ccan/array_size/array_size.h>
+#include <common/setup.h>
+#include <wire/wire.h>
+#include <stdio.h>
+#include <tests/fuzz/libfuzz.h>
+
+#include "../../plugins/funder_policy.c"
+
+/* AUTOGENERATED MOCKS START */
+/* Generated stub for json_add_string */
+void json_add_string(struct json_stream *js UNNEEDED,
+ const char *fieldname UNNEEDED,
+ const char *str TAKES UNNEEDED)
+{ fprintf(stderr, "json_add_string called!\n"); abort(); }
+/* AUTOGENERATED MOCKS END */
+
+#define MAX_SATS ((u64)WALLY_SATOSHI_PER_BTC * WALLY_BTC_MAX)
+
+struct test_case {
+ struct amount_sat their_funds;
+ struct amount_sat available_funds;
+ struct amount_sat *our_last_funds;
+ struct amount_sat max_channel_size;
+ struct amount_sat lease_request;
+ struct funder_policy policy;
+};
+
+static struct amount_sat fromwire_amount_sat_bounded(const u8 **cursor, size_t *max)
+{
+ struct amount_sat amt = fromwire_amount_sat(cursor, max);
+ amt.satoshis %= (MAX_SATS + 1);
+ return amt;
+}
+
+static struct test_case *new_test_case(const tal_t *ctx, const u8 **cursor, size_t *max)
+{
+ struct test_case *tcase = tal(ctx, struct test_case);
+ tcase->their_funds = fromwire_amount_sat_bounded(cursor, max);
+ tcase->available_funds = fromwire_amount_sat_bounded(cursor, max);
+
+ /* Read flag for our_last_funds */
+ u8 flag = fromwire_u8(cursor, max);
+
+ /* Handle our_last_funds conditionally */
+ struct amount_sat *our_last_funds_val = tal(ctx, struct amount_sat);
+ if (flag)
+ {
+ *our_last_funds_val = fromwire_amount_sat_bounded(cursor, max);
+ tcase->our_last_funds = our_last_funds_val;
+ }
+ else
+ tcase->our_last_funds = NULL;
+
+ tcase->max_channel_size = fromwire_amount_sat_bounded(cursor, max);
+ tcase->lease_request = fromwire_amount_sat_bounded(cursor, max);
+
+ tcase->policy.opt = (enum funder_opt)(fromwire_u8(cursor, max) % 3);
+ tcase->policy.mod = fromwire_u64(cursor, max);
+ switch (tcase->policy.opt)
+ {
+ case MATCH:
+ tcase->policy.mod %= 201;
+ break;
+ case AVAILABLE:
+ tcase->policy.mod %= 101;
+ break;
+ case FIXED:
+ tcase->policy.mod %= MAX_SATS + 1;
+ break;
+ default:
+ assert(false && "invalid policy");
+ }
+ tcase->policy.min_their_funding = fromwire_amount_sat_bounded(cursor, max);
+ tcase->policy.max_their_funding = fromwire_amount_sat_bounded(cursor, max);
+
+ if (amount_sat_greater(tcase->policy.min_their_funding, tcase->policy.max_their_funding)) {
+ struct amount_sat tmp = tcase->policy.min_their_funding;
+ tcase->policy.min_their_funding = tcase->policy.max_their_funding;
+ tcase->policy.max_their_funding = tmp;
+ }
+
+ tcase->policy.per_channel_max = fromwire_amount_sat_bounded(cursor, max);
+ tcase->policy.per_channel_min = fromwire_amount_sat_bounded(cursor, max);
+
+ if (amount_sat_greater(tcase->policy.per_channel_min, tcase->policy.per_channel_max)) {
+ struct amount_sat tmp = tcase->policy.per_channel_min;
+ tcase->policy.per_channel_min = tcase->policy.per_channel_max;
+ tcase->policy.per_channel_max = tmp;
+ }
+
+ tcase->policy.fuzz_factor = fromwire_u8(cursor, max) % 101;
+ tcase->policy.reserve_tank = fromwire_amount_sat_bounded(cursor, max);
+ tcase->policy.fund_probability = fromwire_u8(cursor, max) % 101;
+ tcase->policy.leases_only = fromwire_u8(cursor, max) & 1;
+
+ return tcase;
+}
+
+void init(int *argc, char ***argv)
+{}
+
+void run(const u8 *data, size_t size)
+{
+ struct test_case *tcase = new_test_case(tmpctx, &data, &size);
+
+ struct node_id id;
+ memset(&id, 1, sizeof(id));
+ const char *err;
+ struct amount_sat our_funds;
+
+ /* Call the function under test */
+ err = calculate_our_funding(&tcase->policy, id,
+ tcase->their_funds,
+ tcase->our_last_funds,
+ tcase->available_funds,
+ tcase->max_channel_size,
+ tcase->lease_request,
+ &our_funds);
+
+ /* Validate invariants */
+ if (!err)
+ {
+ /* Check total doesn't exceed max_channel_size */
+ struct amount_sat total;
+ if (!amount_sat_add(&total, tcase->their_funds, our_funds)) {
+ fprintf(stderr, "Overflow in total channel capacity\n");
+ abort();
+ }
+ if (amount_sat_greater(total, tcase->max_channel_size)) {
+ fprintf(stderr, "Total channel capacity %"PRIu64" exceeds size %"PRIu64"\n",
+ total.satoshis, tcase->max_channel_size.satoshis);
+ abort();
+ }
+
+ /* Check our_funds is within per-channel limits */
+ if (amount_sat_less(our_funds, tcase->policy.per_channel_min) &&
+ !amount_sat_is_zero(our_funds)) {
+ fprintf(stderr, "our_funds %"PRIu64" < per_channel_min %"PRIu64"\n",
+ our_funds.satoshis, tcase->policy.per_channel_min.satoshis);
+ abort();
+ }
+ if (amount_sat_greater(our_funds, tcase->policy.per_channel_max)) {
+ fprintf(stderr, "our_funds %"PRIu64" > per_max_channel_size %"PRIu64"\n",
+ our_funds.satoshis, tcase->policy.per_channel_max.satoshis);
+ abort();
+ }
+ }
+
+ /* Check available funds constraint */
+ struct amount_sat available_minus_reserve;
+ if (amount_sat_sub(&available_minus_reserve, tcase->available_funds, tcase->policy.reserve_tank)) {
+ if (amount_sat_greater(our_funds, available_minus_reserve)) {
+ fprintf(stderr, "our_funds %"PRIu64" > available %"PRIu64" - reserve %"PRIu64"\n",
+ our_funds.satoshis, tcase->available_funds.satoshis,
+ tcase->policy.reserve_tank.satoshis);
+ abort();
+ }
+ } else if (!amount_sat_eq(our_funds, AMOUNT_SAT(0))) {
+ fprintf(stderr, "Reserve %"PRIu64" >= available %"PRIu64" but our_funds %"PRIu64" != 0\n",
+ tcase->policy.reserve_tank.satoshis, tcase->available_funds.satoshis,
+ our_funds.satoshis);
+ abort();
+ }
+
+ clean_tmpctx();
+}
Why this scored 15/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.