Skip to content

fuzz-tests: Add a test for full_channel operations #8437

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

Chand-ra
Copy link

@Chand-ra Chand-ra commented Aug 5, 2025

Functions defined in channeld/full_channel.h contain channel operations like ADD_HTLC, FULFILL_HTLC, UPDATE_FEERATE, etc. Since they are a critical part of the HTLC state machine and may be influenced by external agents, add a stateful test for them.

Checklist

Before submitting the PR, ensure the following tasks are completed. If an item is not applicable to your PR, please mark it as checked:

  • The changelog has been updated in the relevant commit(s) according to the guidelines.
  • Tests have been added or modified to reflect the changes.
  • Documentation has been reviewed and updated as needed.
  • Related issues have been listed and linked, including any that this PR closes.

CC: @morehouse

Chandra Pratap added 2 commits August 5, 2025 14:48
Changelog-None: Functions defined in `channeld/full_channel.h`
contain channel operations like `ADD_HTLC`, `FULFILL_HTLC`,
`UPDATE_FEERATE`, etc.

Since they are a critical part of the HTLC state machine and may
be influenced by external agents, add a stateful test for them.
Add a minimal input set as a seed corpus for the newly introduced
test. This leads to discovery of interesting code paths faster.
Copy link
Contributor

@morehouse morehouse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice target at a high level.

I think we're missing a lot of opportunities to fuzz invalid/unexpected/rare states, where bugs could be hiding. Some ideas for improvement:

  • Unshackle the fuzzer a bit, so it can pick random values sometimes.
  • Consider allowing the fuzzer to simulate the various commitment dance messages arriving at unexpected times (e.g., sending revoke_and_ack when the peer hasn't updated the commitment yet).
  • Consider allowing the fuzzer to generate multiple HTLC operations before doing the commitment signed dance. This is valid behavior per the spec.

memset(&p_htlc.preimage, (int)p_htlc.id, sizeof(p_htlc.preimage));

if (add_htlc(lchannel, sender, p_htlc.id, &p_htlc.preimage, msat, cltv)) {
add_htlc(rchannel, sender, p_htlc.id, &p_htlc.preimage, msat, cltv);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should assert that the HTLC is successfully added to rchannel here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This immediately causes an assertion failure here because owed = view->lowest_splice_amnt[side] = 0.

struct pending_htlc p_htlc = pending_htlcs[idx];

if (fulfill_htlc(lchannel, p_htlc.sender, p_htlc.id, &p_htlc.preimage)) {
fulfill_htlc(rchannel, p_htlc.sender, p_htlc.id, &p_htlc.preimage);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should assert that the HTLC is successfully fulfilled on rchannel.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This causes an assertion failure here because htlc_get() returns NULL.


enum side sender = (fromwire_u8(&data, &size) % 2) ? REMOTE : LOCAL;
struct amount_msat msat = fromwire_amount_msat_bounded(&data, &size);
u32 cltv = current_blockheight + fromwire_u16(&data, &size);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be too restrictive. The peer could set any CLTV in update_add_htlc.

break;

enum side sender = (fromwire_u8(&data, &size) % 2) ? REMOTE : LOCAL;
struct amount_msat msat = fromwire_amount_msat_bounded(&data, &size);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The peer could set any amount in their update_add_htlc message.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, changing this to:

struct amount_msat msat = fromwire_amount_msat(&data, &size);

causes an assertion failure here in channel_txs().

* recv_revoke_and_ack -> recv_commit -> sending_revoke_and_ack)
* in the correct opener vs. accepter order.
*/
static void exchange_commits(struct channel *channel, enum side side, const struct htlc ***changed_htlcs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be very interesting to fuzz the commitment dance as well. For each of these messages, the channel peer can send all kinds of unexpected fields.

Copy link
Author

@Chand-ra Chand-ra Aug 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how to go about implementing this. The only fields passed to the functions in this routine are channel and changed_htlcs out of which changed_htlcs is set by the said functions, and fuzzing channel would be probably not be worth the trouble since we already create our own channel which is passed here.

Is it fine to fuzz the order in which we call the handlers? Maybe we should create a separate fuzz_exchange_commits() which calls the handlers in random order, and add both of these as new branches in the switch case? I think that would nicely incorporate these feedbacks as well:

  • Consider allowing the fuzzer to generate multiple HTLC operations before doing the commitment signed dance. This is valid behavior per the spec.
  • Consider allowing the fuzzer to simulate the various commitment dance messages arriving at unexpected times (e.g., sending revoke_and_ack when the peer hasn't updated the commitment yet).

Comment on lines +159 to +160
if (!channel_get_htlc(channel, original_sender, id))
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The peer could send an invalid HTLC ID.

Copy link
Author

@Chand-ra Chand-ra Aug 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameters are passed to this caller from run() so it's probably better to make changes there. Maybe something like:

if (fromwire_u8(&data, &size) % 2) {
	if (fail_htlc(lchannel, p_htlc.sender, p_htlc.id)) {
		fail_htlc(rchannel, p_htlc.sender, p_htlc.id);
		tal_arr_remove(&pending_htlcs, idx);
	}
} else {
	fail_htlc(lchannel, fromwire_u8(&data, &size) % 2 ? LOCAL : REMOTE,
	fromwire_u64(&data, &size));
	fail_htlc(rchannel, fromwire_u8(&data, &size) % 2 ? LOCAL : REMOTE,
	fromwire_u64(&data, &size));
}

to make sure both valid and non-valid paths are exercised.

}
case 3: /* UPDATE_FEE */
{
u32 feerate = fromwire_bounded_feerate(&data, &size);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The peer could set any feerate.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be checks that prevent fees too low or too high from reaching the target function, channel_update_feerate(), in the channel daemon. Changing this to fromwire_u32() causes this assertion failure:

fuzz-full_channel: common/fee_states.c:175: u32 marginal_feerate(u32): Assertion `current_feerate >= minfeerate' failed.

where minfeerate is 253.

{
u32 feerate = fromwire_bounded_feerate(&data, &size);
if (update_feerate(lchannel, feerate))
update_feerate(rchannel, feerate);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should assert that the feerate is successfully updated for rchannel.

Comment on lines +344 to +345
u32 height_increase = 1 + fromwire_u32(&data, &size);
current_blockheight += height_increase;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The peer could set any blockheight, including one that is less than the previous one.

Comment on lines +354 to +357
channel_txs(tmpctx, &funding, funding_amount, &htlc_map, NULL, &funding_wscript_alt,
lchannel, &local_per_commitment_point, 42, LOCAL, 0, 0, &anchor, NULL);
channel_txs(tmpctx, &funding, funding_amount, &htlc_map, NULL, &funding_wscript_alt,
rchannel, &local_per_commitment_point, 42, REMOTE, 0, 0, &anchor, NULL);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expensive to generate these transactions? Maybe we should generate them after each loop iteration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants