Skip to content

Comments

Add fuzzers for UTXO validation and gossip verification#4438

Open
tnull wants to merge 2 commits intolightningdevkit:mainfrom
tnull:2026-02-gossip-fix
Open

Add fuzzers for UTXO validation and gossip verification#4438
tnull wants to merge 2 commits intolightningdevkit:mainfrom
tnull:2026-02-gossip-fix

Conversation

@tnull
Copy link
Contributor

@tnull tnull commented Feb 24, 2026

I hit a debug_assert in the gossip verification flow (see #4439) that I'm still trying to reproduce. As a side product I added fuzz targets for UTXO validation and gossip verification.

Add a dedicated fuzz target exercising the UTXO verification logic in
`routing::utxo`, covering the `PendingChecks` state machine through
the `NetworkGraph` and `P2PGossipSync` public APIs.

The `FuzzChainSource` returns fuzz-driven `UtxoResult` variants
including sync success/failure, async with immediate resolution, and
async with deferred resolution. The fuzz loop drives channel
announcements (with and without UTXO lookup), node announcements,
channel updates, deferred future resolution (success and failure),
completed-check processing, and future drops without resolution.

Co-Authored-By: HAL 9000
Add a dedicated fuzz target exercising the `GossipVerifier` from
`lightning-block-sync`, which implements `UtxoLookup` by fetching
blocks and checking UTXOs via the `BlockSource`/`UtxoSource` traits.

The `FuzzBlockSource` drives all async methods from fuzz input,
covering `BlockSourceError` variants (persistent and transient),
`HeaderOnly` vs `FullBlock` responses, variable block shapes
(transaction and output counts), the 5-confirmation depth check, and
the `is_output_unspent` spent/unspent paths. The fuzz loop feeds
channel announcements through the `GossipVerifier`, interleaved with
node announcements, channel updates, and `check_resolved_futures`
processing via `P2PGossipSync`.

Co-Authored-By: HAL 9000
@ldk-reviews-bot
Copy link

👋 Hi! Please choose at least one reviewer by assigning them on the right bar.
If no reviewers are assigned within 10 minutes, I'll automatically assign one.
Once the first reviewer has submitted a review, a second will be assigned if required.

@codecov
Copy link

codecov bot commented Feb 24, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.87%. Comparing base (eb3980d) to head (7bd3705).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4438      +/-   ##
==========================================
- Coverage   85.88%   85.87%   -0.01%     
==========================================
  Files         159      159              
  Lines      104302   104302              
  Branches   104302   104302              
==========================================
- Hits        89578    89568      -10     
- Misses      12222    12232      +10     
  Partials     2502     2502              
Flag Coverage Δ
tests 85.87% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

I'm not quite sure that each of these adds independent test coverage? Should we skip utxo_validation and just do gossip_verifier?

use std::sync::{Arc, Mutex};

#[inline]
pub fn slice_to_be16(v: &[u8]) -> u16 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

heh, these are in std now, we should use std.


macro_rules! decode_msg_with_len16 {
($MsgType: path, $excess: expr) => {{
let extra_len = slice_to_be16(get_slice_nonadvancing!(2));
Copy link
Collaborator

Choose a reason for hiding this comment

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

I assume this was copied from somewhere that's buggy but this is weird? We read 2 bytes of length, but don't advance the buffer, then in decode_msg read those two bytes again as a part of the message.

let idx_byte = get_slice!(1)[0] as usize;
let idx = idx_byte % futures.len();
let future = futures.remove(idx);
let script_byte = get_slice!(1)[0];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we just use the byte that was already read when we called get_utxo so we skip the extra read? Same below, we can decide the ultimate future result when we read in get_utxo rather than here.

// Channel announcement with UTXO lookup
0 => {
let msg = decode_msg_with_len16!(
msgs::UnsignedChannelAnnouncement,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I do wonder if there's any actual value in decoding whole messages - if we instead just read a few fixed bytes and use it to decide between a set of valid messages, can we get the same coverage?

// Process completed checks (triggers check_resolved_futures)
3 => {
// Yield first so any in-flight tokio tasks can resolve their futures.
tokio::task::yield_now().await;
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should only be deterministic if we are using a single-threaded runtime, no? (and even then not really...)

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.

3 participants