Skip to content

smite: add channel_ready oracle - #260

Merged
morehouse merged 6 commits into
lnfuzz:masterfrom
NishantBansal2003:channel-ready-oracle
Oct 15, 2026
Merged

morehouse merged 6 commits into
lnfuzz:masterfrom
NishantBansal2003:channel-ready-oracle

Conversation

@NishantBansal2003

Copy link
Copy Markdown
Member

Depends-on: #212 and #259

Add ChannelReadyOracle and consolidate all previously scattered checks relevant to the channel_ready negotiation into it, including unknown channel_id, cases where the target omits the short_channel_id alias required by option_scid_alias, and cases where the target reuses a per-commitment point from an earlier negotiation.

Deferred oracle checks

  • short_channel_id alias collisions: BOLT 2 requires aliases not to collide with any of the target's real short_channel_ids. Checking this requires fetching the short_channel_id for all channels we have with the target over RPC. Bulk fetching adds RPC overhead that reduces fuzzing throughput, while lazy lookups can still miss collisions. Until this can be checked more efficiently, it is not worthwhile for this narrow surface.

@erickcestari erickcestari left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The last 2 commits LGTM!

Comment thread smite/src/oracles/channel_ready.rs Outdated
Comment thread smite/src/oracles/channel_ready.rs Outdated
Comment thread smite/src/oracles/channel_ready.rs Outdated
Comment on lines +49 to +52
if context
.negotiated_features
.supports_feature(Features::OPTION_SCID_ALIAS)
&& context.channel_ready.tlvs.short_channel_id.is_none()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think it is init rather than channel_type specifically here (see: lightning/bolts#910 (comment)). I think the reason for separating the two is that the one coupled with channel_type enables required privacy, while the one coupled with init says that if the alias is present (which it must be if option_scid_alias is negotiated in init), the peer can resend multiple channel_ready with a different alias as well (see: lightning/bolts#910 (comment), lightning/bolts#910 (comment)).

Comment thread smite/src/oracles/channel_ready.rs
Comment thread smite/src/oracles/channel_ready.rs
Comment thread smite/src/oracles/channel_ready.rs Outdated
@NishantBansal2003
NishantBansal2003 force-pushed the channel-ready-oracle branch 2 times, most recently from 12ede2f to e99f4c9 Compare October 13, 2026 08:25
@NishantBansal2003

NishantBansal2003 commented Oct 13, 2026 •

Copy link
Copy Markdown
Member Author

Some additional checks that could be added:

  • In the accept_channel oracle -- accept_channel.minimum_depth > 2 for non 0-conf channels
  • In the channel_ready oracle -- real_scid != alias for private channels
  • In the channel_ready oracle -- real_scid (of one channel) != alias for other channels

Comment thread smite/src/oracles/channel_ready.rs Outdated
Comment thread smite-scenarios/src/executor/tests.rs
Comment thread smite/src/oracles/channel_ready.rs
Comment thread smite/src/oracles/channel_ready.rs
Comment thread smite/src/oracles/channel_ready.rs
Comment thread smite/src/oracles/channel_ready.rs Outdated
Comment thread smite/src/oracles/channel_ready.rs
Comment thread smite-scenarios/src/executor.rs Outdated
Comment on lines +249 to +256
assert_pass(
&channel_ready(),
Some(&channel_state()),
&Features::new(),
&HashMap::new(),
CONFIRMED_DEPTH,
&HashMap::new(),
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

These parameter lists add a lot of bloat to the tests. It might be worth a test fixture to simplify things:

struct Inputs {
    channel_ready: &ChannelReady,
    channel: Option<&ChannelState>,
    funding_confirmations: u32,
    negotiated_features: &Features,
    revealed_pubkeys: &HashMap<PublicKey, KeyOrigin>,
    revealed_scids: &HashMap<ShortChannelId, ChannelId>,
}
// ... implement ::default() values
// ... implement needed helper methods
fn assert_pass(inputs: &Inputs);
fn assert_fail(inputs: &Inputs, expected: &str);

Then a test only needs to specify the non-default fields:

let inputs = Inputs { funding_confirmations: MINIMUM_DEPTH - 1, ..Inputs::default() };
assert_fail(&inputs, ...);

To avoid churn on this PR, probably better as a follow-up PR.

Comment on lines +1317 to +1326
/// neither an `M` nor implicitly handled, or any error from [`recv_non_ping`].
fn recv_bolt<M: FromMessage>(
conn: &mut impl Connection,
channel_states: &mut HashMap<ChannelId, ChannelState>,
revealed_pubkeys: &mut HashMap<PublicKey, KeyOrigin>,
) -> Result<(), ExecuteError> {
let cr: ChannelReady = recv_bolt(conn, RECV_CHANNEL_READY_TIMEOUT)?;

let state = channel_states
.get_mut(&cr.channel_id)
.ok_or(Violation::UnknownChannel(cr.channel_id))?;
*state.next_counterparty_per_commitment_point_mut() = Some(cr.second_per_commitment_point);

// Record the `second_per_commitment_point` as a per-commitment point the
// target revealed, for commitment number 1. It is unique, so record its
// only origin.
//
// TODO: Once we have the channel_ready oracle, flag a reused
// `second_per_commitment_point`, i.e. one already present in
// `revealed_pubkeys`, unless the entry is a `TargetPcp` for this same
// channel and commitment number. That exception allows the two legitimate
// repeats: the target may resend `channel_ready` with a different alias but
// the same point, and we may receive the same point again after a
// reconnection.
revealed_pubkeys.insert(
cr.second_per_commitment_point,
KeyOrigin::TargetPcp {
channel: cr.channel_id,
commitment_number: 1,
},
);

Ok(())
revealed_scids: &mut HashMap<ShortChannelId, ChannelId>,
context: &ProgramContext,
bitcoin_cli: &mut impl BitcoinRpc,
timeout: Duration,
) -> Result<M, ExecuteError> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The parameter lists for recv_bolt and recv_non_ping are getting very large. We should probably convert them to methods instead (as a follow-up PR).

@morehouse

Copy link
Copy Markdown
Collaborator
* In the `accept_channel` oracle -- `accept_channel.minimum_depth > 2` for non 0-conf channels

Why not > 1 or > 0?

* In the `channel_ready` oracle -- `real_scid != alias` for private channels

* In the `channel_ready` oracle -- `real_scid (of one channel) != alias` for other channels

These seem good to me, though I'd rather add them in follow-ups so this PR doesn't continue to grow.

I also wonder if it's worth checking for premature/unexpected channel_readys when funding_signed hasn't been received or when our funding_created signature was invalid.

@NishantBansal2003

Copy link
Copy Markdown
Member Author

Why not > 1 or > 0?

I was thinking that, on mainnet, 2-block reorgs have already been observed (1 are frequent, while 2 are rare), so it's better to have minimum_depth > 2

These seem good to me, though I'd rather add them in follow-ups so this PR doesn't continue to grow.

I also wonder if it's worth checking for premature/unexpected channel_readys when funding_signed hasn't been received or when our funding_created signature was invalid.

Yes, I was thinking of adding these and the other mentioned checks in a follow-up

}

#[test]
fn execute_recv_channel_ready_rejects_alias_reused_by_another_channel() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Today we could also add a test for one channel's real SCID being reused as another channel's alias.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, I am aware of it, but that is a gap we currently have, and it is not allowed behaviour. So I don't think we should add tests allowing it. I think we should add only a rejection test in a follow-up PR when we reject those cases

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Actually I think we could write the test today, since we record the real SCID in revealed_scids.

The test we couldn't write yet is for the real SCID being used as the alias for the same channel, though an oracle unit test is probably good enough for that one.

Either way works for me, whether we add the test in this PR or a follow up.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I added the case where the second channel_ready uses an alias that matches the real_scid of the first channel. I think the inverse case, where the first channel uses an alias that matches the real_scid of the second channel, could be added in a follow-up once we add the corresponding check

Comment thread smite-scenarios/src/executor.rs Outdated
Comment thread smite-scenarios/src/executor/tests.rs
Comment thread smite-scenarios/src/executor.rs Outdated
Comment thread smite-scenarios/src/executor/tests.rs
Comment thread smite-scenarios/src/executor/tests.rs Outdated
Comment thread smite-scenarios/src/executor/tests.rs Outdated

@morehouse morehouse left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ready for squash and rebase.

}

#[test]
fn execute_recv_channel_ready_rejects_alias_reused_by_another_channel() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Actually I think we could write the test today, since we record the real SCID in revealed_scids.

The test we couldn't write yet is for the real SCID being used as the alias for the same channel, though an oracle unit test is probably good enough for that one.

Either way works for me, whether we add the test in this PR or a follow up.

Comment on lines +1243 to +1271
Message::ChannelReady(ref cr) => {
log::debug!("received channel_ready on {}", cr.channel_id);
let channel = channel_states.get(&cr.channel_id);
// TODO: Both lookups below call `getrawtransaction`. Returning
// confirmations alongside the block position would save one RPC.
let funding_outpoint = channel
.filter(|c| c.is_funding_outpoint_valid)
.map(|c| c.config.funding_outpoint);
let funding_confirmations = funding_outpoint
.map_or(0, |o| bitcoin_cli.get_transaction_confirmations(o.txid));
let funding_scid =
funding_outpoint.and_then(|o| lookup_short_channel_id(bitcoin_cli, o));
ChannelReadyOracle.evaluate(&ChannelReadyContext {
channel_ready: cr,
channel,
negotiated_features: &context.negotiated_features,
revealed_pubkeys,
funding_confirmations,
revealed_scids,
})?;
record_recv_channel_ready(
channel_states,
revealed_pubkeys,
revealed_scids,
funding_scid,
cr,
);
return Ok(msg);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The implicit handling is starting to bloat recv_non_ping into something its name doesn't accurately describe.

I think it would be good to move the implicit handling into a handle_implicit helper called from recv_bolt in a follow up.

// Only the first `channel_ready` sets these. `ChannelReadyOracle` checks
// resends against its point, and a resend's point is stale once the
// commitment state advances, so it must not overwrite the next point.
if state.received_channel_ready_point.is_none() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we could also simplify is_channel_ready_expected slightly by checking the new received_channel_ready_point. Fine as a follow up.

Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
@morehouse
morehouse merged commit 089ca0c into lnfuzz:master Oct 15, 2026
5 checks passed
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