diff --git a/config_builder.go b/config_builder.go index 6f9cb1f3e98..d3ca5e2b275 100644 --- a/config_builder.go +++ b/config_builder.go @@ -213,6 +213,11 @@ type AuxComponents struct { // AuxContractResolver is an optional interface that can be used to // modify the way contracts are resolved. AuxContractResolver fn.Option[lnwallet.AuxContractResolver] + + // AuxChannelNegotiator is an optional interface that allows aux channel + // implementations to inject and process custom records over channel + // related wire messages. + AuxChannelNegotiator fn.Option[lnwallet.AuxChannelNegotiator] } // DefaultWalletImpl is the default implementation of our normal, btcwallet diff --git a/docs/release-notes/release-notes-0.20.0.md b/docs/release-notes/release-notes-0.20.0.md index 50653dcffdf..f237d3bb914 100644 --- a/docs/release-notes/release-notes-0.20.0.md +++ b/docs/release-notes/release-notes-0.20.0.md @@ -76,6 +76,11 @@ a certain amount of msats. [allow](https://github.com/lightningnetwork/lnd/pull/10087) `conf_target=1`. Previously they required `conf_target >= 2`. +* A new AuxComponent was added named AuxChannelNegotiator. This component aids + with custom data communication for aux channels, by injecting and handling + data in channel related wire messages. See + [PR](https://github.com/lightningnetwork/lnd/pull/10182) for more info. + ## RPC Additions * When querying [`ForwardingEvents`](https://github.com/lightningnetwork/lnd/pull/9813) logs, the response now include the incoming and outgoing htlc indices of the payment diff --git a/funding/manager.go b/funding/manager.go index 33d57328ea3..4bfb7f9d094 100644 --- a/funding/manager.go +++ b/funding/manager.go @@ -568,6 +568,11 @@ type Config struct { // AuxResolver is an optional interface that can be used to modify the // way contracts are resolved. AuxResolver fn.Option[lnwallet.AuxContractResolver] + + // AuxChannelNegotiator is an optional interface that allows aux channel + // implementations to inject and process custom records over channel + // related wire messages. + AuxChannelNegotiator fn.Option[lnwallet.AuxChannelNegotiator] } // Manager acts as an orchestrator/bridge between the wallet's @@ -4019,6 +4024,14 @@ func (f *Manager) handleChannelReady(peer lnpeer.Peer, //nolint:funlen defer f.wg.Done() + // Notify the aux hook that the specified peer just established a + // channel with us, identified by the given channel ID. + f.cfg.AuxChannelNegotiator.WhenSome( + func(acn lnwallet.AuxChannelNegotiator) { + acn.ProcessChannelReady(msg.ChanID, peer.PubKey()) + }, + ) + // If we are in development mode, we'll wait for specified duration // before processing the channel ready message. if f.cfg.Dev != nil { diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 2d1dd7de010..4c81964cc24 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -298,6 +298,11 @@ type ChannelLinkConfig struct { // used to manage the bandwidth of the link. AuxTrafficShaper fn.Option[AuxTrafficShaper] + // AuxChannelNegotiator is an optional interface that allows aux channel + // implementations to inject and process custom records over channel + // related wire messages. + AuxChannelNegotiator fn.Option[lnwallet.AuxChannelNegotiator] + // QuiescenceTimeout is the max duration that the channel can be // quiesced. Any dependent protocols (dynamic commitments, splicing, // etc.) must finish their operations under this timeout value, @@ -987,6 +992,22 @@ func (l *channelLink) syncChanStates(ctx context.Context) error { // In any case, we'll then process their ChanSync message. l.log.Info("received re-establishment message from remote side") + // If we have an AuxChannelNegotiator we notify any external + // component for this message. This serves as a notification + // that the reestablish message was received. + l.cfg.AuxChannelNegotiator.WhenSome( + func(acn lnwallet.AuxChannelNegotiator) { + fundingPoint := l.channel.ChannelPoint() + cid := lnwire.NewChanIDFromOutPoint( + fundingPoint, + ) + + acn.ProcessReestablish( + cid, l.cfg.Peer.PubKey(), + ) + }, + ) + var ( openedCircuits []CircuitKey closedCircuits []CircuitKey diff --git a/lnwallet/aux_leaf_store.go b/lnwallet/aux_leaf_store.go index aa2cc3b3e95..0a850503780 100644 --- a/lnwallet/aux_leaf_store.go +++ b/lnwallet/aux_leaf_store.go @@ -9,6 +9,7 @@ import ( "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/tlv" ) @@ -97,6 +98,10 @@ type AuxChanState struct { // funding output. TapscriptRoot fn.Option[chainhash.Hash] + // PeerPubKey is the peer pub key of the peer we've established this + // channel with. + PeerPubKey route.Vertex + // CustomBlob is an optional blob that can be used to store information // specific to a custom channel type. This information is only created // at channel funding time, and after wards is to be considered @@ -106,6 +111,8 @@ type AuxChanState struct { // NewAuxChanState creates a new AuxChanState from the given channel state. func NewAuxChanState(chanState *channeldb.OpenChannel) AuxChanState { + peerPub := chanState.IdentityPub.SerializeCompressed() + return AuxChanState{ ChanType: chanState.ChanType, FundingOutpoint: chanState.FundingOutpoint, @@ -116,6 +123,7 @@ func NewAuxChanState(chanState *channeldb.OpenChannel) AuxChanState { RemoteChanCfg: chanState.RemoteChanCfg, ThawHeight: chanState.ThawHeight, TapscriptRoot: chanState.TapscriptRoot, + PeerPubKey: route.Vertex(peerPub), CustomBlob: chanState.CustomBlob, } } diff --git a/lnwallet/aux_negotiator.go b/lnwallet/aux_negotiator.go new file mode 100644 index 00000000000..73096a7d0cf --- /dev/null +++ b/lnwallet/aux_negotiator.go @@ -0,0 +1,34 @@ +package lnwallet + +import ( + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +// AuxChannelNegotiator is an interface that allows aux channel implementations +// to inject or handle custom records in the init message that is used when +// establishing a connection with a peer. It may also notify the aux channel +// implementation for the channel ready or channel reestablish events, which +// mark the channel as ready to use. +type AuxChannelNegotiator interface { + // GetInitRecords is called when sending an init message to a peer. + // It returns custom records to include in the init message TLVs. The + // implementation can decide which records to include based on the peer + // identity. + GetInitRecords(peer route.Vertex) (lnwire.CustomRecords, error) + + // ProcessInitRecords handles received init records from a peer. The + // implementation can store state internally to affect future + // channel operations with this peer. + ProcessInitRecords(peer route.Vertex, + customRecords lnwire.CustomRecords) error + + // ProcessChannelReady handles the event of marking a channel identified + // by its channel ID as ready to use. We also provide the peer the + // channel was established with. + ProcessChannelReady(cid lnwire.ChannelID, peer route.Vertex) + + // ProcessReestablish handles the received channel_reestablish message + // which marks a channel identified by its cid as ready to use again. + ProcessReestablish(cid lnwire.ChannelID, peer route.Vertex) +} diff --git a/lnwire/init_message.go b/lnwire/init_message.go index b88891b0870..dd3b4714226 100644 --- a/lnwire/init_message.go +++ b/lnwire/init_message.go @@ -24,6 +24,10 @@ type Init struct { // Features field. Features *RawFeatureVector + // CustomRecords maps TLV types to byte slices, storing arbitrary data + // intended for inclusion in the ExtraData field of the init message. + CustomRecords CustomRecords + // 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. @@ -35,7 +39,6 @@ func NewInitMessage(gf *RawFeatureVector, f *RawFeatureVector) *Init { return &Init{ GlobalFeatures: gf, Features: f, - ExtraData: make([]byte, 0), } } @@ -52,11 +55,28 @@ var _ SizeableMessage = (*Init)(nil) // // This is part of the lnwire.Message interface. func (msg *Init) Decode(r io.Reader, pver uint32) error { - return ReadElements(r, + var msgExtraData ExtraOpaqueData + + err := ReadElements(r, &msg.GlobalFeatures, &msg.Features, - &msg.ExtraData, + &msgExtraData, ) + if err != nil { + return err + } + + customRecords, _, extraDData, err := ParseAndExtractCustomRecords( + msgExtraData, + ) + if err != nil { + return err + } + + msg.CustomRecords = customRecords + msg.ExtraData = extraDData + + return nil } // Encode serializes the target Init into the passed io.Writer observing @@ -72,7 +92,12 @@ func (msg *Init) Encode(w *bytes.Buffer, pver uint32) error { return err } - return WriteBytes(w, msg.ExtraData) + extraData, err := MergeAndEncode(nil, msg.ExtraData, msg.CustomRecords) + if err != nil { + return err + } + + return WriteBytes(w, extraData) } // MsgType returns the integer uniquely identifying this message type on the diff --git a/lnwire/init_message_test.go b/lnwire/init_message_test.go new file mode 100644 index 00000000000..d0a3acaffb0 --- /dev/null +++ b/lnwire/init_message_test.go @@ -0,0 +1,61 @@ +package lnwire + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestInitEncodeDecode checks that we can encode and decode an Init message +// to and from a byte stream. +func TestInitEncodeDecode(t *testing.T) { + t.Parallel() + + // These are the raw bytes that we expect to be generated from the + // sample Init message. + rawBytes := []byte{ + // GlobalFeatures + 0x00, 0x01, 0xc0, + + // Features + 0x00, 0x01, 0xc0, + + // ExtraData - unknown odd-type TLV record. + 0x6f, // type (111) + 0x02, // length + 0x79, 0x79, // value + + // ExtraData - custom TLV record. + // TLV record for type 67676 + 0xfe, 0x00, 0x01, 0x08, 0x6c, // type (67676) + 0x05, // length + 0x01, 0x02, 0x03, 0x04, 0x05, // value + + // ExtraData - custom TLV record. + // TLV record for type 67777 + 0xfe, 0x00, 0x01, 0x08, 0xc1, // type (67777) + 0x03, // length + 0x01, 0x02, 0x03, // value + } + + // Create a new empty message and decode the raw bytes into it. + msg := &Init{} + r := bytes.NewReader(rawBytes) + err := msg.Decode(r, 0) + require.NoError(t, err) + + require.NotNil(t, msg.GlobalFeatures) + require.NotNil(t, msg.Features) + require.NotNil(t, msg.CustomRecords) + require.NotNil(t, msg.ExtraData) + + // Next, encode the message back into a new byte buffer. + var b bytes.Buffer + err = msg.Encode(&b, 0) + require.NoError(t, err) + + // The re-encoded bytes should be exactly the same as the original raw + // bytes. + require.Equal(t, rawBytes, b.Bytes()) +} diff --git a/lnwire/test_message.go b/lnwire/test_message.go index 8f946f190e9..0c2fe5e55d2 100644 --- a/lnwire/test_message.go +++ b/lnwire/test_message.go @@ -1188,6 +1188,10 @@ func (msg *Init) RandTestMessage(t *rapid.T) Message { local.Set(bit) } + ignoreRecords := fn.NewSet[uint64]() + + msg.ExtraData = RandExtraOpaqueData(t, ignoreRecords) + return NewInitMessage(global, local) } diff --git a/peer/brontide.go b/peer/brontide.go index 57e340fb0e8..1ea16d1e811 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -456,6 +456,11 @@ type Config struct { // used to modify the way the co-op close transaction is constructed. AuxChanCloser fn.Option[chancloser.AuxChanCloser] + // AuxChannelNegotiator is an optional interface that allows aux channel + // implementations to inject and process custom records over channel + // related wire messages. + AuxChannelNegotiator fn.Option[lnwallet.AuxChannelNegotiator] + // ShouldFwdExpEndorsement is a closure that indicates whether // experimental endorsement signals should be set. ShouldFwdExpEndorsement func() bool @@ -1454,8 +1459,9 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint, ShouldFwdExpEndorsement: p.cfg.ShouldFwdExpEndorsement, DisallowQuiescence: p.cfg.DisallowQuiescence || !p.remoteFeatures.HasFeature(lnwire.QuiescenceOptional), - AuxTrafficShaper: p.cfg.AuxTrafficShaper, - QuiescenceTimeout: p.cfg.QuiescenceTimeout, + AuxTrafficShaper: p.cfg.AuxTrafficShaper, + AuxChannelNegotiator: p.cfg.AuxChannelNegotiator, + QuiescenceTimeout: p.cfg.QuiescenceTimeout, } // Before adding our new link, purge the switch of any pending or live @@ -4537,6 +4543,19 @@ func (p *Brontide) handleInitMsg(msg *lnwire.Init) error { return fmt.Errorf("data loss protection required") } + // If we have an AuxChannelNegotiator and the peer sent aux features, + // process them. + p.cfg.AuxChannelNegotiator.WhenSome( + func(acn lnwallet.AuxChannelNegotiator) { + err = acn.ProcessInitRecords( + p.cfg.PubKeyBytes, msg.CustomRecords.Copy(), + ) + }, + ) + if err != nil { + return fmt.Errorf("could not process init records: %w", err) + } + return nil } @@ -4597,6 +4616,30 @@ func (p *Brontide) sendInitMsg(legacyChan bool) error { features.RawFeatureVector, ) + var err error + + // If we have an AuxChannelNegotiator, get custom feature bits to + // include in the init message. + p.cfg.AuxChannelNegotiator.WhenSome( + func(negotiator lnwallet.AuxChannelNegotiator) { + var auxRecords lnwire.CustomRecords + auxRecords, err = negotiator.GetInitRecords( + p.cfg.PubKeyBytes, + ) + if err != nil { + p.log.Warnf("Failed to get aux init features: "+ + "%v", err) + return + } + + mergedRecs := msg.CustomRecords.MergedCopy(auxRecords) + msg.CustomRecords = mergedRecs + }, + ) + if err != nil { + return err + } + return p.writeMessage(msg) } diff --git a/server.go b/server.go index 44be180ea9b..ac3894012bc 100644 --- a/server.go +++ b/server.go @@ -1628,6 +1628,7 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr, AuxFundingController: implCfg.AuxFundingController, AuxSigner: implCfg.AuxSigner, AuxResolver: implCfg.AuxContractResolver, + AuxChannelNegotiator: implCfg.AuxChannelNegotiator, }) if err != nil { return nil, err @@ -4431,6 +4432,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq, AuxChanCloser: s.implCfg.AuxChanCloser, AuxResolver: s.implCfg.AuxContractResolver, AuxTrafficShaper: s.implCfg.TrafficShaper, + AuxChannelNegotiator: s.implCfg.AuxChannelNegotiator, ShouldFwdExpEndorsement: func() bool { if s.cfg.ProtocolOptions.NoExperimentalEndorsement() { return false