Skip to content
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

lnwire: add wire types for dynamic commitments #8026

Merged
merged 6 commits into from
Nov 10, 2023

Conversation

ProofOfKeags
Copy link
Collaborator

@ProofOfKeags ProofOfKeags commented Sep 23, 2023

Change Description

This PR introduces new datatypes to LND as described in the Dynamic Commitments spec.

Steps to Test

make unit

Pull Request Checklist

Testing

  • Your PR passes all CI checks.
  • Tests covering the positive and negative (error paths) are included.
  • Bug fixes contain tests triggering the bug to prevent regressions.

Code Style and Documentation

📝 Please see our Contribution Guidelines for further guidance.

@ProofOfKeags ProofOfKeags changed the base branch from master to 0-18-staging September 23, 2023 02:37
@ProofOfKeags ProofOfKeags marked this pull request as ready for review September 25, 2023 23:02
@ProofOfKeags ProofOfKeags marked this pull request as draft September 25, 2023 23:02
@ProofOfKeags ProofOfKeags force-pushed the feature/dyncomms branch 2 times, most recently from 7ec0860 to a48aa9a Compare September 26, 2023 03:30
//
// This is a part of the lnwire.Message interface.
func (dbp *DynBeginPropose) Encode(w *bytes.Buffer, _ uint32) error {
if err := WriteChannelID(w, dbp.ChanID); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

We should still read in the ExtraOpaqueData here. Otherwise we may end up with just a partially consumed buffer.

Copy link
Member

Choose a reason for hiding this comment

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

I guess we also still need to decide which portions of the message are TLV vs otherwise.

lnwire/dyn_propose.go Outdated Show resolved Hide resolved
lnwire/dyn_propose.go Outdated Show resolved Hide resolved
lnwire/dyn_propose.go Outdated Show resolved Hide resolved
lnwire/dyn_propose_test.go Outdated Show resolved Hide resolved
lnwire/dyn_begin_propose.go Outdated Show resolved Hide resolved
lnwire/dyn_propose.go Outdated Show resolved Hide resolved
lnwire/dyn_propose.go Show resolved Hide resolved
lnwire/dyn_propose.go Outdated Show resolved Hide resolved
lnwire/dyn_propose.go Outdated Show resolved Hide resolved
lnwire/dyn_propose.go Show resolved Hide resolved
@saubyk saubyk added this to the v0.18.0 milestone Oct 3, 2023
@saubyk saubyk assigned ProofOfKeags and unassigned ProofOfKeags Oct 5, 2023
@Crypt-iQ
Copy link
Collaborator

Crypt-iQ commented Oct 9, 2023

Think this should come with fuzz tests for the new message types. Can just do round-trip equality w/ decode + encode

lnwire/dyn_ack.go Outdated Show resolved Hide resolved
lnwire/dyn_ack.go Outdated Show resolved Hide resolved
lnwire/dyn_propose.go Outdated Show resolved Hide resolved
lnwire/dyn_reject.go Outdated Show resolved Hide resolved
@ProofOfKeags
Copy link
Collaborator Author

Think this should come with fuzz tests for the new message types. Can just do round-trip equality w/ decode + encode

It does.

@saubyk saubyk linked an issue Oct 12, 2023 that may be closed by this pull request
18 tasks
@Crypt-iQ
Copy link
Collaborator

The more modern fuzz tests are in lnwire/fuzz_test.go. The go fuzzing engine is a bit better than testing/quick mutation. You can probably use the existing harness function for each of the new messages here:

lnd/lnwire/fuzz_test.go

Lines 26 to 54 in ca9cdac

func harness(t *testing.T, data []byte) {
t.Helper()
// Create a reader with the byte array.
r := bytes.NewReader(data)
// Check that the created message is not greater than the maximum
// message size.
if len(data) > MaxSliceLength {
return
}
msg, err := ReadMessage(r, 0)
if err != nil {
return
}
// We will serialize the message into a new bytes buffer.
var b bytes.Buffer
_, err = WriteMessage(&b, msg, 0)
require.NoError(t, err)
// Deserialize the message from the serialized bytes buffer, and then
// assert that the original message is equal to the newly deserialized
// message.
newMsg, err := ReadMessage(&b, 0)
require.NoError(t, err)
require.Equal(t, msg, newMsg)
}

@ProofOfKeags ProofOfKeags force-pushed the feature/dyncomms branch 3 times, most recently from 4dc6cba to 32601d4 Compare October 14, 2023 18:42
@ProofOfKeags ProofOfKeags changed the title lnwire: add wire types for dynamic commitment negotation messages lnwire: add wire types for dynamic commitments Oct 19, 2023
@ProofOfKeags ProofOfKeags force-pushed the feature/dyncomms branch 2 times, most recently from 889dd1f to 81e3e83 Compare October 19, 2023 03:51
@ProofOfKeags ProofOfKeags marked this pull request as ready for review October 19, 2023 03:51
@saubyk saubyk requested review from Roasbeef and Crypt-iQ October 19, 2023 15:24
Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

Message structure looks pretty good.

Open to trying out Option[T] in place of the pointer usage as an initial pilot on the cobdebase. Here's one I prototyped in the past: https://go.dev/play/p/l1R_ueg2NoJ. I think siimlar to tapd, we'd add an fn package, drop this in, then go from there.

Other thing we need to nail down is the initial version of the reject bitvector/message we want go along with.

lnwire/dyn_ack.go Show resolved Hide resolved
lnwire/dyn_propose.go Show resolved Hide resolved
lnwire/dyn_propose.go Outdated Show resolved Hide resolved
lnwire/dyn_propose.go Show resolved Hide resolved
// the initiator of a dynamic commitment negotiation or the responder
// of a dynamic commitment negotiation. bool true indicates it is the
// initiator
Initiator bool
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this information already apparent to both sides contextually?

Eg: for channel funding we know who initiator is from the very start, so we don't need messages like closing_signed to signal if it's sent by the initiator not (for that msg behavior differs based on if you're the initiator or not).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What do we do in the situation where both sides send a dyn_propose "at the same time"? It's unlikely in practice but possible. I suppose we could just fail at some point when the conflict becomes unignorable. This was put in to express intent to the receiver that we believe we are initiating the negotiation. Such an intent isn't clear otherwise.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this can be fixed if you abort the flow if you send a dyn_propose with initiator=true and receive a dyn_propose with initiator=true

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed. But absent the initiator=true label I don't think this can be accomplished.

lnwire/dyn_propose.go Outdated Show resolved Hide resolved
var csvDelayScratch uint16
csvDelay := tlv.MakePrimitiveRecord(DPToSelfDelay, &csvDelayScratch)

var maxHtlcsScratch uint16
Copy link
Member

Choose a reason for hiding this comment

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

If the target isn't a non-primitive type, then you can decode directly into it. Should be able to save ~20 lines or so here w/ that.

var csvDelayScratch uint16
csvDelay := tlv.MakePrimitiveRecord(DPToSelfDelay, &csvDelayScratch)

var maxHtlcsScratch uint16
Copy link
Member

Choose a reason for hiding this comment

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

Similar here re if not primitive type, can pass the attribute directly in.

@lightninglabs-deploy
Copy link

@Crypt-iQ: review reminder
@ProofOfKeags, remember to re-request review from reviewers when ready

Comment on lines +594 to +600
case *RawFeatureVector:
f := NewRawFeatureVector()
err = f.Decode(r)
if err != nil {
return err
}
*e = *f
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Is this a sin?

@ProofOfKeags ProofOfKeags force-pushed the feature/dyncomms branch 3 times, most recently from 1ec5cce to ecf9483 Compare October 31, 2023 19:33
@ProofOfKeags ProofOfKeags requested a review from Roasbeef October 31, 2023 19:35
lnwire/dyn_propose.go Show resolved Hide resolved
})
dp.FundingKey.WhenSome(func(key btcec.PublicKey) {
keyScratch := &key
tlvRecords = append(tlvRecords, tlv.MakePrimitiveRecord(
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: should have a line break before tlvRecords similar to how ChannelType is below

)
})
dp.KickoffFeerate.WhenSome(func(kickoffFeerate chainfee.SatPerKWeight) {
protoSats := uint32(kickoffFeerate)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note for later implementation:
kickoffFeerate is SatPerKWeight which is int64 under the hood. Casting to uint32 can lead to integer truncation if the feerate is very high or low. To avoid headaches and possible bugs, we should ensure that whatever kickoff feerate we send/receive is <= math.MaxUint32

dp.ChannelType = fn.Some(chanTypeScratch)
}
if val, ok := typeMap[DPKickoffFeerate]; ok && val == nil {
dp.KickoffFeerate = fn.Some(chainfee.SatPerKWeight(
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: newline for chainfee.SatPerKWeight to avoid double parenthesis

lnwire/dyn_reject.go Show resolved Hide resolved
@@ -708,6 +710,100 @@ func TestLightningWireProtocol(t *testing.T) {

v[0] = reflect.ValueOf(req)
},
MsgDynPropose: func(v []reflect.Value, r *rand.Rand) {
var dp DynPropose
var id ChannelID
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: id unnecessary?

},
MsgDynReject: func(v []reflect.Value, r *rand.Rand) {
var dr DynReject
var id ChannelID
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: id unnecessary?

lnwire/lnwire_test.go Show resolved Hide resolved
this commit introduces many of the most common functions you will
want to use with the Option type. Not all of them are used
immediately in this PR.
@ProofOfKeags ProofOfKeags force-pushed the feature/dyncomms branch 3 times, most recently from 00fc24c to 134fa78 Compare November 8, 2023 02:06
@ProofOfKeags ProofOfKeags requested a review from Crypt-iQ November 8, 2023 02:09
Copy link
Member

@Roasbeef Roasbeef left a comment

Choose a reason for hiding this comment

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

LGTM ✏️


// Option[A] represents a value which may or may not be there. This is very
// often preferable to nil-able pointers.
type Option[A any] struct {
Copy link
Member

Choose a reason for hiding this comment

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

🤩

// DynHeight is an optional field that stores the dynamic commitment
// negotiation height that is incremented upon successful completion of
// a dynamic commitment negotiation
DynHeight *DynHeight
Copy link
Member

Choose a reason for hiding this comment

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

Could be an optional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed. Should I also do the same thing to the LocalNonce field even though it's unrelated to dyncomms or leave it heterogeneous for now?

lnwire/dyn_reject.go Show resolved Hide resolved
lnwire/dyn_reject.go Show resolved Hide resolved
lnwire/dyn_propose.go Show resolved Hide resolved
lnwire/lnwire_test.go Show resolved Hide resolved
@Roasbeef Roasbeef merged this pull request into lightningnetwork:0-18-staging Nov 10, 2023
24 of 25 checks passed
@ProofOfKeags ProofOfKeags mentioned this pull request Nov 11, 2023
7 tasks
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.

[epic]: Dynamic Commitments (Dynamic Channels)
5 participants