Skip to content

multi: record channel negotiation state and fold build/send operations#134

Open
NishantBansal2003 wants to merge 4 commits into
morehouse:masterfrom
NishantBansal2003:channel-negotiation
Open

multi: record channel negotiation state and fold build/send operations#134
NishantBansal2003 wants to merge 4 commits into
morehouse:masterfrom
NishantBansal2003:channel-negotiation

Conversation

@NishantBansal2003

Copy link
Copy Markdown
Contributor

Fixes: #130

Store negotiated open_channel/accept_channel messages during execution so later commitment steps use the parameters actually exchanged on the wire.

This change is accompanied by BuildFundingCreated using the negotiated state to build the channel state used for signing and verification. Also, all the states are now recorded on send, so there is no state corruption between the target and smite if we store a different state from what was actually sent to the peer.

Mostly, this change is done so that we can extend this approach to add open_channel <-> accept_channel oracles, which require a mapping between these two messages. This change lays the groundwork so we can add those oracles in subsequent PRs.

Store negotiated open_channel/accept_channel messages
during execution so later commitment steps use the
parameters actually exchanged on the wire.

This lets BuildFundingCreated derive the negotiated
parameters from temporary_channel_id, reducing its
inputs and ensuring commitment transaction signing
and signature verification always use the negotiated
state stored by the executor.

This change also intentionally does not fold BuildOpenChannel
into SendOpenChannel. Both require many inputs, and keeping
them separate makes it easier to test duplicate open_channel
send cases.

Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Build the funding_created message from the channel parameters
negotiated during the open_channel/accept_channel exchange.
This ensures commitment transaction signing and signature
verification always use the negotiated state stored by the
executor, rather than separate inputs.

Reducing the inputs to just the required parameters also makes
it possible to test invalid-signature cases while ensuring the
stored negotiation state remains correct.

Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Fold BuildFundingCreated into SendFundingCreated. Since the
intermediate funding_created message is not used elsewhere,
merging the two operations simplifies the API.

Unlike BuildOpenChannel and SendOpenChannel, which remain
separate because of their large number of inputs and the need
to test duplicate open_channel sends, funding_created requires
far fewer inputs. This makes it straightforward to exercise
duplicate send cases even after combining the build and send steps.

Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Fold BuildChannelReady into SendChannelReady. Since the
intermediate channel_ready message is not used elsewhere,
merging the two operations simplifies the API.

As with funding_created, channel_ready requires relatively
few inputs, making it straightforward to exercise duplicate
send cases even after combining the build and send steps.

Signed-off-by: Nishant Bansal <nishant.bansal.282003@gmail.com>
Comment on lines +1135 to +1138
let open_channel = match Message::decode(bytes) {
Ok(Message::OpenChannel(oc)) => oc,
other => panic!("SendOpenChannel did not produce an open_channel message: {other:?}"),
};

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nit: we now do an extra open_channel decode. Perhaps we should make the OpenChannelMessage type contain an OpenChannel instead of a Vec<u8> to avoid the unnecessary work.

Comment on lines +1158 to +1163
let pending = negotiations
.get_mut(&accept_channel.temporary_channel_id)
.ok_or(ExecuteError::UnknownChannel(
accept_channel.temporary_channel_id,
))?;
pending.accept_channel = Some(accept_channel.clone());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Theoretically, if we sent two open_channel messages with the same temporary_channel_id, we would record the first one. But if the target responded to both, we would record the second accept_channel, which would not align with the recorded open_channel message.

I would be surprised if any implementation actually responds to the second open_channel, and I'm not sure what the best action is in that case, but just wanted to flag this for discussion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

temporary_channel_id reuse should be rejected until the previous negotiation reaches funding_created. So if the target sends a second accept_channel reusing the same temporary_channel_id, I think we should return an error

Comment on lines +2210 to +2211
let accept_channel = pending.accept_channel.as_ref().unwrap();
assert_eq!(accept_channel.temporary_channel_id, temporary_channel_id);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Nit: we could check that the entire accept_channel message is equal to sample_accept_channel().

Comment on lines +727 to +735
let pending = negotiations
.get(&temporary_channel_id)
.ok_or(ExecuteError::IncompleteNegotiation(temporary_channel_id))?;
let open_channel = &pending.open_channel;
let accept_channel = pending
.accept_channel
.as_ref()
.ok_or(ExecuteError::IncompleteNegotiation(temporary_channel_id))?;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

By returning IncompleteNegotiation, we lose all ability to send funding_created out of order. I think it would be better to send some default-constructed funding_created when we're missing the data we need for a valid signature.

Something like this:

  • temporary_channel_id, funding_txid, and funding_output_index determined from inputs
  • signature is all zeroes
  • nothing saved to channel_states

Comment on lines 786 to 790
// Building the same message again must not clobber a channel whose state
// has already been established (and possibly advanced).
channel_states
.entry(channel_id)
.or_insert_with(|| ChannelState::new(config, holder, state));

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I think we also want to either delete or mark the entry in negotiations so that we're able to open another channel using the same temporary_channel_id and record the parameters for that channel.

The simplest approach is to delete the entry from negotiations once the channel "graduates" to a real channel_id. But if we want to keep the ability to easily send duplicate funding_created messages, we can also keep the entry around and just set a flag on the negotiations entry to mark the channel as "graduated". Graduated entries can still be used for subsequent funding_created messages but also get overwritten by the next SendOpenChannel that uses the same temporary_channel_id.

@NishantBansal2003

Copy link
Copy Markdown
Contributor Author

I was also concerned about temporary_channel_id reuse, but BOLT 2 says:

The sending node:

  • MUST ensure the chain_hash value identifies the chain it wishes to open the channel within.
  • MUST ensure temporary_channel_id is unique from any other channel ID with the same peer.

Based on that, I expected implementations to reject any reused temporary_channel_id, even if the previous channel had already transitioned to the active state.

But, I checked the code in CLN and LDK (and also verified it by running https://github.com/NishantBansal2003/smite/tree/funding-created-generator), and it turns out that once funding_created is sent, the entry for the temporary_channel_id is removed. So, the same peer can send another open_channel with the same temporary_channel_id, and both implementations accept it

CLN: https://github.com/ElementsProject/lightning/blob/58f0c16a57e324529d8ce9f40e6511d31b004c0e/connectd/multiplex.c#L73-L78
LDK: https://github.com/lightningdevkit/rust-lightning/blob/759378c912c3a9ca86d312bbf7929550644ca174/lightning/src/ln/channelmanager.rs#L11755-L11758

if we want to keep the ability to easily send duplicate funding_created messages, we can also keep the entry around and just set a flag on the negotiations entry to mark the channel as "graduated". Graduated entries can still be used for subsequent funding_created messages but also get overwritten by the next SendOpenChannel that uses the same temporary_channel_id.

I think this is the better way to handle such cases

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.

invalid counterparty signature false-positive crashes in funding flow generator

2 participants