Skip to content

Commit

Permalink
Merge branch 'dyncomms'
Browse files Browse the repository at this point in the history
  • Loading branch information
Roasbeef committed Nov 10, 2023
2 parents a3f8011 + 9793fbb commit 9937b1d
Show file tree
Hide file tree
Showing 11 changed files with 982 additions and 1 deletion.
5 changes: 5 additions & 0 deletions docs/release-notes/release-notes-0.18.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@

# Technical and Architectural Updates
## BOLT Spec Updates

* [Add Dynamic Commitment Wire Types](https://github.com/lightningnetwork/lnd/pull/8026).
This change begins the development of Dynamic Commitments allowing for the
negotiation of new channel parameters and the upgrading of channel types.

## Testing

* Added fuzz tests for [onion
Expand Down
149 changes: 149 additions & 0 deletions fn/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package fn

// 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 {
isSome bool
some A
}

// Some trivially injects a value into an optional context.
//
// Some : A -> Option[A].
func Some[A any](a A) Option[A] {
return Option[A]{
isSome: true,
some: a,
}
}

// None trivially constructs an empty option
//
// None : Option[A].
func None[A any]() Option[A] {
return Option[A]{}
}

// ElimOption is the universal Option eliminator. It can be used to safely
// handle all possible values inside the Option by supplying two continuations.
//
// ElimOption : (Option[A], () -> B, A -> B) -> B.
func ElimOption[A, B any](o Option[A], b func() B, f func(A) B) B {
if o.isSome {
return f(o.some)
}

return b()
}

// UnwrapOr is used to extract a value from an option, and we supply the default
// value in the case when the Option is empty.
//
// UnwrapOr : (Option[A], A) -> A.
func (o Option[A]) UnwrapOr(a A) A {
if o.isSome {
return o.some
}

return a
}

// WhenSome is used to conditionally perform a side-effecting function that
// accepts a value of the type that parameterizes the option. If this function
// performs no side effects, WhenSome is useless.
//
// WhenSome : (Option[A], A -> ()) -> ().
func (o Option[A]) WhenSome(f func(A)) {
if o.isSome {
f(o.some)
}
}

// IsSome returns true if the Option contains a value
//
// IsSome : Option[A] -> bool.
func (o Option[A]) IsSome() bool {
return o.isSome
}

// IsNone returns true if the Option is empty
//
// IsNone : Option[A] -> bool.
func (o Option[A]) IsNone() bool {
return !o.isSome
}

// FlattenOption joins multiple layers of Options together such that if any of
// the layers is None, then the joined value is None. Otherwise the innermost
// Some value is returned.
//
// FlattenOption : Option[Option[A]] -> Option[A].
func FlattenOption[A any](oo Option[Option[A]]) Option[A] {
if oo.IsNone() {
return None[A]()
}
if oo.some.IsNone() {
return None[A]()
}

return oo.some
}

// ChainOption transforms a function A -> Option[B] into one that accepts an
// Option[A] as an argument.
//
// ChainOption : (A -> Option[B]) -> Option[A] -> Option[B].
func ChainOption[A, B any](f func(A) Option[B]) func(Option[A]) Option[B] {
return func(o Option[A]) Option[B] {
if o.isSome {
return f(o.some)
}

return None[B]()
}
}

// MapOption transforms a pure function A -> B into one that will operate
// inside the Option context.
//
// MapOption : (A -> B) -> Option[A] -> Option[B].
func MapOption[A, B any](f func(A) B) func(Option[A]) Option[B] {
return func(o Option[A]) Option[B] {
if o.isSome {
return Some(f(o.some))
}

return None[B]()
}
}

// LiftA2Option transforms a pure function (A, B) -> C into one that will
// operate in an Option context. For the returned function, if either of its
// arguments are None, then the result will be None.
//
// LiftA2Option : ((A, B) -> C) -> (Option[A], Option[B]) -> Option[C].
func LiftA2Option[A, B, C any](
f func(A, B) C,
) func(Option[A], Option[B]) Option[C] {

return func(o1 Option[A], o2 Option[B]) Option[C] {
if o1.isSome && o2.isSome {
return Some(f(o1.some, o2.some))
}

return None[C]()
}
}

// Alt chooses the left Option if it is full, otherwise it chooses the right
// option. This can be useful in a long chain if you want to choose between
// many different ways of producing the needed value.
//
// Alt : Option[A] -> Option[A] -> Option[A].
func (o Option[A]) Alt(o2 Option[A]) Option[A] {
if o.isSome {
return o
}

return o2
}
30 changes: 29 additions & 1 deletion lnwire/channel_reestablish.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,24 @@ import (
"io"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/tlv"
)

const (
CRDynHeight tlv.Type = 20
)

// DynHeight is a newtype wrapper to get the proper RecordProducer instance
// to smoothly integrate with the ChannelReestablish Message instance.
type DynHeight uint64

// Record implements the RecordProducer interface, allowing a full tlv.Record
// object to be constructed from a DynHeight.
func (d *DynHeight) Record() tlv.Record {
return tlv.MakePrimitiveRecord(CRDynHeight, (*uint64)(d))
}

// ChannelReestablish is a message sent between peers that have an existing
// open channel upon connection reestablishment. This message allows both sides
// to report their local state, and their current knowledge of the state of the
Expand Down Expand Up @@ -70,6 +85,11 @@ type ChannelReestablish struct {
// TODO(roasbeef): rename to verification nonce
LocalNonce *Musig2Nonce

// DynHeight is an optional field that stores the dynamic commitment
// negotiation height that is incremented upon successful completion of
// a dynamic commitment negotiation
DynHeight fn.Option[DynHeight]

// ExtraData is the set of data that was appended to this message to
// fill out the full maximum transport message size. These fields can
// be used to specify optional data such as custom TLV fields.
Expand Down Expand Up @@ -121,6 +141,10 @@ func (a *ChannelReestablish) Encode(w *bytes.Buffer, pver uint32) error {
if a.LocalNonce != nil {
recordProducers = append(recordProducers, a.LocalNonce)
}
a.DynHeight.WhenSome(func(h DynHeight) {
recordProducers = append(recordProducers, &h)
})

err := EncodeMessageExtraData(&a.ExtraData, recordProducers...)
if err != nil {
return err
Expand Down Expand Up @@ -180,8 +204,9 @@ func (a *ChannelReestablish) Decode(r io.Reader, pver uint32) error {
}

var localNonce Musig2Nonce
var dynHeight DynHeight
typeMap, err := tlvRecords.ExtractRecords(
&localNonce,
&localNonce, &dynHeight,
)
if err != nil {
return err
Expand All @@ -190,6 +215,9 @@ func (a *ChannelReestablish) Decode(r io.Reader, pver uint32) error {
if val, ok := typeMap[NonceRecordType]; ok && val == nil {
a.LocalNonce = &localNonce
}
if val, ok := typeMap[CRDynHeight]; ok && val == nil {
a.DynHeight = fn.Some(dynHeight)
}

if len(tlvRecords) != 0 {
a.ExtraData = tlvRecords
Expand Down
138 changes: 138 additions & 0 deletions lnwire/dyn_ack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package lnwire

import (
"bytes"
"io"

"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/tlv"
)

const (
// DALocalMusig2Pubnonce is the TLV type number that identifies the
// musig2 public nonce that we need to verify the commitment transaction
// signature.
DALocalMusig2Pubnonce tlv.Type = 0
)

// DynAck is the message used to accept the parameters of a dynamic commitment
// negotiation. Additional optional parameters will need to be present depending
// on the details of the dynamic commitment upgrade.
type DynAck struct {
// ChanID is the ChannelID of the channel that is currently undergoing
// a dynamic commitment negotiation
ChanID ChannelID

// LocalNonce is an optional field that is transmitted when accepting
// a dynamic commitment upgrade to Taproot Channels. This nonce will be
// used to verify the first commitment transaction signature. This will
// only be populated if the DynPropose we are responding to specifies
// taproot channels in the ChannelType field.
LocalNonce fn.Option[Musig2Nonce]

// ExtraData is the set of data that was appended to this message to
// fill out the full maximum transport message size. These fields can
// be used to specify optional data such as custom TLV fields.
ExtraData ExtraOpaqueData
}

// A compile time check to ensure DynAck implements the lnwire.Message
// interface.
var _ Message = (*DynAck)(nil)

// Encode serializes the target DynAck into the passed io.Writer. Serialization
// will observe the rules defined by the passed protocol version.
//
// This is a part of the lnwire.Message interface.
func (da *DynAck) Encode(w *bytes.Buffer, _ uint32) error {
if err := WriteChannelID(w, da.ChanID); err != nil {
return err
}

var tlvRecords []tlv.Record
da.LocalNonce.WhenSome(func(nonce Musig2Nonce) {
tlvRecords = append(
tlvRecords, tlv.MakeStaticRecord(
DALocalMusig2Pubnonce, &nonce,
musig2.PubNonceSize, nonceTypeEncoder,
nonceTypeDecoder,
),
)
})
tlv.SortRecords(tlvRecords)

tlvStream, err := tlv.NewStream(tlvRecords...)
if err != nil {
return err
}

var extraBytesWriter bytes.Buffer
if err := tlvStream.Encode(&extraBytesWriter); err != nil {
return err
}

da.ExtraData = ExtraOpaqueData(extraBytesWriter.Bytes())

return WriteBytes(w, da.ExtraData)
}

// Decode deserializes the serialized DynAck stored in the passed io.Reader into
// the target DynAck using the deserialization rules defined by the passed
// protocol version.
//
// This is a part of the lnwire.Message interface.
func (da *DynAck) Decode(r io.Reader, _ uint32) error {
// Parse out main message.
if err := ReadElements(r, &da.ChanID); err != nil {
return err
}

// Parse out TLV records.
var tlvRecords ExtraOpaqueData
if err := ReadElement(r, &tlvRecords); err != nil {
return err
}

// Prepare receiving buffers to be filled by TLV extraction.
var localNonceScratch Musig2Nonce
localNonce := tlv.MakeStaticRecord(
DALocalMusig2Pubnonce, &localNonceScratch, musig2.PubNonceSize,
nonceTypeEncoder, nonceTypeDecoder,
)

// Create set of Records to read TLV bytestream into.
records := []tlv.Record{localNonce}
tlv.SortRecords(records)

// Read TLV stream into record set.
extraBytesReader := bytes.NewReader(tlvRecords)
tlvStream, err := tlv.NewStream(records...)
if err != nil {
return err
}
typeMap, err := tlvStream.DecodeWithParsedTypesP2P(extraBytesReader)
if err != nil {
return err
}

// Check the results of the TLV Stream decoding and appropriately set
// message fields.
if val, ok := typeMap[DALocalMusig2Pubnonce]; ok && val == nil {
da.LocalNonce = fn.Some(localNonceScratch)
}

if len(tlvRecords) != 0 {
da.ExtraData = tlvRecords
}

return nil
}

// MsgType returns the MessageType code which uniquely identifies this message
// as a DynAck on the wire.
//
// This is part of the lnwire.Message interface.
func (da *DynAck) MsgType() MessageType {
return MsgDynAck
}
Loading

0 comments on commit 9937b1d

Please sign in to comment.