diff --git a/discovery/gossiper.go b/discovery/gossiper.go index 0b59c8af996..d6855ae4547 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -3257,6 +3257,7 @@ func (d *AuthenticatedGossiper) handleChanUpdate(ctx context.Context, MaxHTLC: upd.HtlcMaximumMsat, FeeBaseMSat: lnwire.MilliSatoshi(upd.BaseFee), FeeProportionalMillionths: lnwire.MilliSatoshi(upd.FeeRate), + InboundFee: upd.InboundFee.ValOpt(), ExtraOpaqueData: upd.ExtraOpaqueData, } diff --git a/docs/release-notes/release-notes-0.20.0.md b/docs/release-notes/release-notes-0.20.0.md index 7bc9d6e8e27..4515378701b 100644 --- a/docs/release-notes/release-notes-0.20.0.md +++ b/docs/release-notes/release-notes-0.20.0.md @@ -88,6 +88,11 @@ circuit. The indices are only available for forwarding events saved after v0.20. # Technical and Architectural Updates ## BOLT Spec Updates +* Explicitly define the [inbound fee TLV + record](https://github.com/lightningnetwork/lnd/pull/9897) on the + `channel_update` message and handle it explicitly throughout the code base + instead of extracting it from the TLV stream at various call-sites. + ## Testing ## Database @@ -99,4 +104,5 @@ circuit. The indices are only available for forwarding events saved after v0.20. # Contributors (Alphabetical Order) * Abdulkbk -Pins +* Elle Mouton +* Pins diff --git a/go.mod b/go.mod index d5716a01d53..f7f6b08ff02 100644 --- a/go.mod +++ b/go.mod @@ -203,6 +203,9 @@ require ( // store have been included in a tagged sqldb version. replace github.com/lightningnetwork/lnd/sqldb => ./sqldb +// TODO(elle): replace once the updated tlv package has been tagged. +replace github.com/lightningnetwork/lnd/tlv => ./tlv + // This replace is for https://github.com/advisories/GHSA-25xm-hr59-7c27 replace github.com/ulikunitz/xz => github.com/ulikunitz/xz v0.5.11 diff --git a/go.sum b/go.sum index 40665b84a12..5760d9bd9bf 100644 --- a/go.sum +++ b/go.sum @@ -377,8 +377,6 @@ github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQ github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4= github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM= github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA= -github.com/lightningnetwork/lnd/tlv v1.3.1 h1:o7CZg06y+rJZfUMAo0WzBLr0pgBWCzrt0f9gpujYUzk= -github.com/lightningnetwork/lnd/tlv v1.3.1/go.mod h1:pJuiBj1ecr1WWLOtcZ+2+hu9Ey25aJWFIsjmAoPPnmc= github.com/lightningnetwork/lnd/tor v1.1.6 h1:WHUumk7WgU6BUFsqHuqszI9P6nfhMeIG+rjJBlVE6OE= github.com/lightningnetwork/lnd/tor v1.1.6/go.mod h1:qSRB8llhAK+a6kaTPWOLLXSZc6Hg8ZC0mq1sUQ/8JfI= github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw= diff --git a/graph/builder.go b/graph/builder.go index 3350eeb3319..99fc73c17cb 100644 --- a/graph/builder.go +++ b/graph/builder.go @@ -943,7 +943,7 @@ func (b *Builder) ApplyChannelUpdate(msg *lnwire.ChannelUpdate1) bool { return false } - err = b.UpdateEdge(&models.ChannelEdgePolicy{ + update := &models.ChannelEdgePolicy{ SigBytes: msg.Signature.ToSignatureBytes(), ChannelID: msg.ShortChannelID.ToUint64(), LastUpdate: time.Unix(int64(msg.Timestamp), 0), @@ -954,8 +954,11 @@ func (b *Builder) ApplyChannelUpdate(msg *lnwire.ChannelUpdate1) bool { MaxHTLC: msg.HtlcMaximumMsat, FeeBaseMSat: lnwire.MilliSatoshi(msg.BaseFee), FeeProportionalMillionths: lnwire.MilliSatoshi(msg.FeeRate), + InboundFee: msg.InboundFee.ValOpt(), ExtraOpaqueData: msg.ExtraOpaqueData, - }) + } + + err = b.UpdateEdge(update) if err != nil && !IsError(err, ErrIgnored, ErrOutdated) { log.Errorf("Unable to apply channel update: %v", err) return false diff --git a/graph/db/errors.go b/graph/db/errors.go index afdbdb457ee..8201980aaaf 100644 --- a/graph/db/errors.go +++ b/graph/db/errors.go @@ -12,6 +12,10 @@ var ( ErrEdgePolicyOptionalFieldNotFound = fmt.Errorf("optional field not " + "present") + // ErrParsingExtraTLVBytes is returned when we attempt to parse + // extra opaque bytes as a TLV stream, but the parsing fails. + ErrParsingExtraTLVBytes = fmt.Errorf("error parsing extra TLV bytes") + // ErrGraphNotFound is returned when at least one of the components of // graph doesn't exist. ErrGraphNotFound = fmt.Errorf("graph bucket not initialized") diff --git a/graph/db/graph_cache.go b/graph/db/graph_cache.go index cefa88a856e..141f610b261 100644 --- a/graph/db/graph_cache.go +++ b/graph/db/graph_cache.go @@ -180,17 +180,6 @@ func (c *GraphCache) updateOrAddEdge(node route.Vertex, edge *DirectedChannel) { func (c *GraphCache) UpdatePolicy(policy *models.ChannelEdgePolicy, fromNode, toNode route.Vertex, edge1 bool) { - // Extract inbound fee if possible and available. If there is a decoding - // error, ignore this policy. - var inboundFee lnwire.Fee - _, err := policy.ExtraOpaqueData.ExtractRecords(&inboundFee) - if err != nil { - log.Errorf("Failed to extract records from edge policy %v: %v", - policy.ChannelID, err) - - return - } - c.mtx.Lock() defer c.mtx.Unlock() @@ -216,13 +205,17 @@ func (c *GraphCache) UpdatePolicy(policy *models.ChannelEdgePolicy, fromNode, // policy for node 1. case channel.IsNode1 && edge1: channel.OutPolicySet = true - channel.InboundFee = inboundFee + policy.InboundFee.WhenSome(func(fee lnwire.Fee) { + channel.InboundFee = fee + }) // This is node 2, and it is edge 2, so this is the outgoing // policy for node 2. case !channel.IsNode1 && !edge1: channel.OutPolicySet = true - channel.InboundFee = inboundFee + policy.InboundFee.WhenSome(func(fee lnwire.Fee) { + channel.InboundFee = fee + }) // The other two cases left mean it's the inbound policy for the // node. diff --git a/graph/db/graph_cache_test.go b/graph/db/graph_cache_test.go index 087fb81acc5..96318366da5 100644 --- a/graph/db/graph_cache_test.go +++ b/graph/db/graph_cache_test.go @@ -4,6 +4,7 @@ import ( "encoding/hex" "testing" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" @@ -37,11 +38,17 @@ func TestGraphCacheAddNode(t *testing.T) { channelFlagA, channelFlagB = 1, 0 } + inboundFee := lnwire.Fee{ + BaseFee: 10, + FeeRate: 20, + } + outPolicy1 := &models.ChannelEdgePolicy{ ChannelID: 1000, ChannelFlags: lnwire.ChanUpdateChanFlags(channelFlagA), ToNode: nodeB, // Define an inbound fee. + InboundFee: fn.Some(inboundFee), ExtraOpaqueData: []byte{ 253, 217, 3, 8, 0, 0, 0, 10, 0, 0, 0, 20, }, diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go index c8a5c54b20e..efe1b4642f4 100644 --- a/graph/db/graph_test.go +++ b/graph/db/graph_test.go @@ -4195,19 +4195,25 @@ func TestGraphCacheForEachNodeChannel(t *testing.T) { directedChan := getSingleChannel() require.NotNil(t, directedChan) - require.Equal(t, directedChan.InboundFee, lnwire.Fee{ + expectedInbound := lnwire.Fee{ BaseFee: 10, FeeRate: 20, - }) + } + require.Equal(t, expectedInbound, directedChan.InboundFee) - // Set an invalid inbound fee and check that the edge is no longer - // returned. + // Set an invalid inbound fee and check that persistence fails. edge1.ExtraOpaqueData = []byte{ 253, 217, 3, 8, 0, } - require.NoError(t, graph.UpdateEdgePolicy(edge1)) + require.ErrorIs( + t, graph.UpdateEdgePolicy(edge1), ErrParsingExtraTLVBytes, + ) - require.Nil(t, getSingleChannel()) + // Since persistence of the last update failed, we should still bet + // the previous result when we query the channel again. + directedChan = getSingleChannel() + require.NotNil(t, directedChan) + require.Equal(t, expectedInbound, directedChan.InboundFee) } // TestGraphLoading asserts that the cache is properly reconstructed after a diff --git a/graph/db/kv_store.go b/graph/db/kv_store.go index b08c86022d7..51a1d848306 100644 --- a/graph/db/kv_store.go +++ b/graph/db/kv_store.go @@ -22,6 +22,7 @@ import ( "github.com/btcsuite/btcwallet/walletdb" "github.com/lightningnetwork/lnd/aliasmgr" "github.com/lightningnetwork/lnd/batch" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/kvdb" @@ -284,6 +285,13 @@ func (c *KVStore) getChannelMap(edges kvdb.RBucket) ( case errors.Is(err, ErrEdgePolicyOptionalFieldNotFound): return nil + // We don't want a single policy with bad TLV data to stop us + // from loading the rest of the data, so we just skip this + // policy. This is for backwards compatibility since we did not + // use to validate TLV data in the past before persisting it. + case errors.Is(err, ErrParsingExtraTLVBytes): + return nil + case err != nil: return err } @@ -474,16 +482,6 @@ func (c *KVStore) forEachNodeDirectedChannel(tx kvdb.RTx, cachedInPolicy.ToNodeFeatures = toNodeFeatures } - var inboundFee lnwire.Fee - if p1 != nil { - // Extract inbound fee. If there is a decoding error, - // skip this edge. - _, err := p1.ExtraOpaqueData.ExtractRecords(&inboundFee) - if err != nil { - return nil - } - } - directedChannel := &DirectedChannel{ ChannelID: e.ChannelID, IsNode1: node == e.NodeKey1Bytes, @@ -491,7 +489,12 @@ func (c *KVStore) forEachNodeDirectedChannel(tx kvdb.RTx, Capacity: e.Capacity, OutPolicySet: p1 != nil, InPolicy: cachedInPolicy, - InboundFee: inboundFee, + } + + if p1 != nil { + p1.InboundFee.WhenSome(func(fee lnwire.Fee) { + directedChannel.InboundFee = fee + }) } if node == e.NodeKey2Bytes { @@ -2342,7 +2345,7 @@ func (c *KVStore) FilterChannelRange(startHeight, edge, err := deserializeChanEdgePolicyRaw(r) if err != nil && !errors.Is( err, ErrEdgePolicyOptionalFieldNotFound, - ) { + ) && !errors.Is(err, ErrParsingExtraTLVBytes) { return err } @@ -2357,7 +2360,7 @@ func (c *KVStore) FilterChannelRange(startHeight, edge, err := deserializeChanEdgePolicyRaw(r) if err != nil && !errors.Is( err, ErrEdgePolicyOptionalFieldNotFound, - ) { + ) && !errors.Is(err, ErrParsingExtraTLVBytes) { return err } @@ -4334,15 +4337,20 @@ func putChanEdgePolicy(edges kvdb.RwBucket, edge *models.ChannelEdgePolicy, // need to deserialize the existing policy within the database // (now outdated by the new one), and delete its corresponding // entry within the update index. We'll ignore any - // ErrEdgePolicyOptionalFieldNotFound error, as we only need - // the channel ID and update time to delete the entry. + // ErrEdgePolicyOptionalFieldNotFound or ErrParsingExtraTLVBytes + // errors, as we only need the channel ID and update time to + // delete the entry. + // // TODO(halseth): get rid of these invalid policies in a // migration. + // TODO(elle): complete the above TODO in migration from kvdb + // to SQL. oldEdgePolicy, err := deserializeChanEdgePolicy( bytes.NewReader(edgeBytes), ) if err != nil && - !errors.Is(err, ErrEdgePolicyOptionalFieldNotFound) { + !errors.Is(err, ErrEdgePolicyOptionalFieldNotFound) && + !errors.Is(err, ErrParsingExtraTLVBytes) { return err } @@ -4449,6 +4457,11 @@ func fetchChanEdgePolicy(edges kvdb.RBucket, chanID []byte, case errors.Is(err, ErrEdgePolicyOptionalFieldNotFound): return nil, nil + // If the policy contains invalid TLV bytes, we return nil as if + // the policy was unknown. + case errors.Is(err, ErrParsingExtraTLVBytes): + return nil, nil + case err != nil: return nil, err } @@ -4546,6 +4559,12 @@ func serializeChanEdgePolicy(w io.Writer, edge *models.ChannelEdgePolicy, } } + // Validate that the ExtraOpaqueData is in fact a valid TLV stream. + err = edge.ExtraOpaqueData.ValidateTLV() + if err != nil { + return fmt.Errorf("%w: %w", ErrParsingExtraTLVBytes, err) + } + if len(edge.ExtraOpaqueData) > MaxAllowedExtraOpaqueBytes { return ErrTooManyExtraOpaqueBytes(len(edge.ExtraOpaqueData)) } @@ -4562,15 +4581,18 @@ func serializeChanEdgePolicy(w io.Writer, edge *models.ChannelEdgePolicy, func deserializeChanEdgePolicy(r io.Reader) (*models.ChannelEdgePolicy, error) { // Deserialize the policy. Note that in case an optional field is not - // found, both an error and a populated policy object are returned. - edge, deserializeErr := deserializeChanEdgePolicyRaw(r) - if deserializeErr != nil && - !errors.Is(deserializeErr, ErrEdgePolicyOptionalFieldNotFound) { + // found or if the edge has invalid TLV data, then both an error and a + // populated policy object are returned so that the caller can decide + // if it still wants to use the edge or not. + edge, err := deserializeChanEdgePolicyRaw(r) + if err != nil && + !errors.Is(err, ErrEdgePolicyOptionalFieldNotFound) && + !errors.Is(err, ErrParsingExtraTLVBytes) { - return nil, deserializeErr + return nil, err } - return edge, deserializeErr + return edge, err } func deserializeChanEdgePolicyRaw(r io.Reader) (*models.ChannelEdgePolicy, @@ -4657,6 +4679,22 @@ func deserializeChanEdgePolicyRaw(r io.Reader) (*models.ChannelEdgePolicy, edge.ExtraOpaqueData = opq[8:] } + // Attempt to extract the inbound fee from the opaque data. If we fail + // to parse the TLV here, we return an error we also return the edge + // so that the caller can still use it. This is for backwards + // compatibility in case we have already persisted some policies that + // have invalid TLV data. + var inboundFee lnwire.Fee + typeMap, err := edge.ExtraOpaqueData.ExtractRecords(&inboundFee) + if err != nil { + return edge, fmt.Errorf("%w: %w", ErrParsingExtraTLVBytes, err) + } + + val, ok := typeMap[lnwire.FeeRecordType] + if ok && val == nil { + edge.InboundFee = fn.Some(inboundFee) + } + return edge, nil } diff --git a/graph/db/models/channel_edge_policy.go b/graph/db/models/channel_edge_policy.go index 365acbfe137..48d748ee0ab 100644 --- a/graph/db/models/channel_edge_policy.go +++ b/graph/db/models/channel_edge_policy.go @@ -5,6 +5,7 @@ import ( "time" "github.com/btcsuite/btcd/btcec/v2/ecdsa" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/lnwire" ) @@ -65,6 +66,15 @@ type ChannelEdgePolicy struct { // to. Using this pub key, the channel graph can further be traversed. ToNode [33]byte + // InboundFee is the fee that must be paid for incoming HTLCs. + // + // NOTE: for our kvdb implementation of the graph store, inbound fees + // are still only persisted as part of extra opaque data and so this + // field is not explicitly stored but is rather populated from the + // ExtraOpaqueData field on deserialization. For our SQL implementation, + // this field will be explicitly persisted in the database. + InboundFee fn.Option[lnwire.Fee] + // ExtraOpaqueData is the set of data that was appended to this // message, some of which we may not actually know how to iterate or // parse. By holding onto this data, we ensure that we're able to diff --git a/graph/db/notifications.go b/graph/db/notifications.go index d573e0aa56d..6c0acba8895 100644 --- a/graph/db/notifications.go +++ b/graph/db/notifications.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/go-errors/errors" + "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnwire" @@ -360,6 +361,9 @@ type ChannelEdgeUpdate struct { // payments. Disabled bool + // InboundFee is the fee that must be paid for incoming HTLCs. + InboundFee fn.Option[lnwire.Fee] + // ExtraOpaqueData 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. @@ -442,6 +446,7 @@ func (c *ChannelGraph) addToTopologyChange(update *TopologyChange, AdvertisingNode: aNode, ConnectingNode: cNode, Disabled: m.ChannelFlags&lnwire.ChanUpdateDisabled != 0, + InboundFee: m.InboundFee, ExtraOpaqueData: m.ExtraOpaqueData, } diff --git a/graph/notifications_test.go b/graph/notifications_test.go index 71c6f4a34e9..2f2ec1b4b4c 100644 --- a/graph/notifications_test.go +++ b/graph/notifications_test.go @@ -121,6 +121,7 @@ func randEdgePolicy(chanID *lnwire.ShortChannelID, FeeBaseMSat: lnwire.MilliSatoshi(prand.Int31()), FeeProportionalMillionths: lnwire.MilliSatoshi(prand.Int31()), ToNode: node.PubKeyBytes, + InboundFee: fn.Some(inboundFee), ExtraOpaqueData: extraOpaqueData, }, nil } diff --git a/lnwire/channel_update.go b/lnwire/channel_update.go index 55d9d3181aa..558d81a9e91 100644 --- a/lnwire/channel_update.go +++ b/lnwire/channel_update.go @@ -6,6 +6,7 @@ import ( "io" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightningnetwork/lnd/tlv" ) // ChanUpdateMsgFlags is a bitfield that signals whether optional fields are @@ -114,6 +115,10 @@ type ChannelUpdate1 struct { // HtlcMaximumMsat is the maximum HTLC value which will be accepted. HtlcMaximumMsat MilliSatoshi + // InboundFee is an optional TLV record that contains the fee + // information for incoming HTLCs. + InboundFee tlv.OptionalRecordT[tlv.TlvType55555, Fee] + // 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. @@ -156,12 +161,27 @@ func (a *ChannelUpdate1) Decode(r io.Reader, _ uint32) error { } } - err = a.ExtraOpaqueData.Decode(r) + var tlvRecords ExtraOpaqueData + if err := ReadElements(r, &tlvRecords); err != nil { + return err + } + + var inboundFee = a.InboundFee.Zero() + typeMap, err := tlvRecords.ExtractRecords(&inboundFee) if err != nil { return err } - return a.ExtraOpaqueData.ValidateTLV() + val, ok := typeMap[a.InboundFee.TlvType()] + if ok && val == nil { + a.InboundFee = tlv.SomeRecordT(inboundFee) + } + + if len(tlvRecords) != 0 { + a.ExtraOpaqueData = tlvRecords + } + + return nil } // Encode serializes the target ChannelUpdate into the passed io.Writer @@ -218,6 +238,16 @@ func (a *ChannelUpdate1) Encode(w *bytes.Buffer, pver uint32) error { } } + recordProducers := make([]tlv.RecordProducer, 0, 1) + a.InboundFee.WhenSome(func(fee tlv.RecordT[tlv.TlvType55555, Fee]) { + recordProducers = append(recordProducers, &fee) + }) + + err := EncodeMessageExtraData(&a.ExtraOpaqueData, recordProducers...) + if err != nil { + return err + } + // Finally, append any extra opaque data. return WriteBytes(w, a.ExtraOpaqueData) } diff --git a/lnwire/test_message.go b/lnwire/test_message.go index 8b3d98400a1..9d086c8688a 100644 --- a/lnwire/test_message.go +++ b/lnwire/test_message.go @@ -12,6 +12,7 @@ import ( "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/tlv" + "github.com/stretchr/testify/require" "pgregory.net/rapid" ) @@ -406,6 +407,45 @@ func (a *ChannelUpdate1) RandTestMessage(t *rapid.T) Message { maxHtlc = 0 } + // Randomly decide if an inbound fee should be included. + // By default, our extra opaque data will just be random TLV but if we + // include an inbound fee, then we will also set the record in the + // extra opaque data. + var ( + customRecords, _ = RandCustomRecords(t, nil, false) + inboundFee tlv.OptionalRecordT[tlv.TlvType55555, Fee] + ) + includeInboundFee := rapid.Bool().Draw(t, "includeInboundFee") + if includeInboundFee { + if customRecords == nil { + customRecords = make(CustomRecords) + } + + inFeeBase := int32( + rapid.IntRange(-1000, 1000).Draw(t, "inFeeBase"), + ) + inFeeProp := int32( + rapid.IntRange(-1000, 1000).Draw(t, "inFeeProp"), + ) + fee := Fee{ + BaseFee: inFeeBase, + FeeRate: inFeeProp, + } + inboundFee = tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType55555, Fee](fee), + ) + + var b bytes.Buffer + feeRecord := fee.Record() + err := feeRecord.Encode(&b) + require.NoError(t, err) + + customRecords[uint64(FeeRecordType)] = b.Bytes() + } + + extraBytes, err := customRecords.Serialize() + require.NoError(t, err) + return &ChannelUpdate1{ Signature: RandSignature(t), ChainHash: hash, @@ -428,7 +468,8 @@ func (a *ChannelUpdate1) RandTestMessage(t *rapid.T) Message { t, "feeRate"), ), HtlcMaximumMsat: maxHtlc, - ExtraOpaqueData: RandExtraOpaqueData(t, nil), + InboundFee: inboundFee, + ExtraOpaqueData: extraBytes, } } diff --git a/netann/channel_update.go b/netann/channel_update.go index efc5cf61e49..1bb509b08bd 100644 --- a/netann/channel_update.go +++ b/netann/channel_update.go @@ -13,6 +13,7 @@ import ( "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" "github.com/pkg/errors" ) @@ -138,7 +139,7 @@ func ExtractChannelUpdate(ownerPubKey []byte, func UnsignedChannelUpdateFromEdge(info *models.ChannelEdgeInfo, policy *models.ChannelEdgePolicy) *lnwire.ChannelUpdate1 { - return &lnwire.ChannelUpdate1{ + update := &lnwire.ChannelUpdate1{ ChainHash: info.ChainHash, ShortChannelID: lnwire.NewShortChanIDFromInt(policy.ChannelID), Timestamp: uint32(policy.LastUpdate.Unix()), @@ -151,6 +152,13 @@ func UnsignedChannelUpdateFromEdge(info *models.ChannelEdgeInfo, FeeRate: uint32(policy.FeeProportionalMillionths), ExtraOpaqueData: policy.ExtraOpaqueData, } + policy.InboundFee.WhenSome(func(fee lnwire.Fee) { + update.InboundFee = tlv.SomeRecordT( + tlv.NewRecordT[tlv.TlvType55555, lnwire.Fee](fee), + ) + }) + + return update } // ChannelUpdateFromEdge reconstructs a signed ChannelUpdate from the given edge diff --git a/peer/brontide.go b/peer/brontide.go index 26d02d3e718..5d00e14ab9e 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -1205,27 +1205,17 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) ( // routing policy into a forwarding policy. var forwardingPolicy *models.ForwardingPolicy if selfPolicy != nil { - var inboundWireFee lnwire.Fee - _, err := selfPolicy.ExtraOpaqueData.ExtractRecords( - &inboundWireFee, - ) - if err != nil { - return nil, err - } - - inboundFee := models.NewInboundFeeFromWire( - inboundWireFee, - ) - forwardingPolicy = &models.ForwardingPolicy{ MinHTLCOut: selfPolicy.MinHTLC, MaxHTLC: selfPolicy.MaxHTLC, BaseFee: selfPolicy.FeeBaseMSat, FeeRate: selfPolicy.FeeProportionalMillionths, TimeLockDelta: uint32(selfPolicy.TimeLockDelta), - - InboundFee: inboundFee, } + selfPolicy.InboundFee.WhenSome(func(fee lnwire.Fee) { + inboundFee := models.NewInboundFeeFromWire(fee) + forwardingPolicy.InboundFee = inboundFee + }) } else { p.log.Warnf("Unable to find our forwarding policy "+ "for channel %v, using default values", diff --git a/routing/localchans/manager.go b/routing/localchans/manager.go index 3beb28525c1..492f8f18c28 100644 --- a/routing/localchans/manager.go +++ b/routing/localchans/manager.go @@ -127,12 +127,10 @@ func (r *Manager) UpdatePolicy(newSchema routing.ChannelPolicy, Edge: edge, }) - // Extract inbound fees from the ExtraOpaqueData. var inboundWireFee lnwire.Fee - _, err = edge.ExtraOpaqueData.ExtractRecords(&inboundWireFee) - if err != nil { - return err - } + edge.InboundFee.WhenSome(func(fee lnwire.Fee) { + inboundWireFee = fee + }) inboundFee := models.NewInboundFeeFromWire(inboundWireFee) // Add updated policy to list of policies to send to switch. @@ -372,6 +370,8 @@ func (r *Manager) updateEdge(chanPoint wire.OutPoint, err = fn.MapOptionZ(newSchema.InboundFee, func(f models.InboundFee) error { inboundWireFee := f.ToWire() + edge.InboundFee = fn.Some(inboundWireFee) + return edge.ExtraOpaqueData.PackRecords( &inboundWireFee, ) diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index bc3a9ef58b3..361fb83d4d8 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -666,15 +666,25 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, return nil, err } - getExtraData := func( - end *testChannelEnd) lnwire.ExtraOpaqueData { + getInboundFees := func( + end *testChannelEnd) fn.Option[lnwire.Fee] { - var extraData lnwire.ExtraOpaqueData inboundFee := lnwire.Fee{ BaseFee: int32(end.InboundFeeBaseMsat), FeeRate: int32(end.InboundFeeRate), } - require.NoError(t, extraData.PackRecords(&inboundFee)) + + return fn.Some(inboundFee) + } + getExtraData := func( + end *testChannelEnd) lnwire.ExtraOpaqueData { + + var extraData lnwire.ExtraOpaqueData + + inboundFee := getInboundFees(end) + inboundFee.WhenSome(func(fee lnwire.Fee) { + require.NoError(t, extraData.PackRecords(&fee)) + }) return extraData } @@ -701,6 +711,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, FeeBaseMSat: node1.FeeBaseMsat, FeeProportionalMillionths: node1.FeeRate, ToNode: node2Vertex, + InboundFee: getInboundFees(node1), //nolint:ll ExtraOpaqueData: getExtraData(node1), } if err := graph.UpdateEdgePolicy(edgePolicy); err != nil { @@ -731,6 +742,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool, FeeBaseMSat: node2.FeeBaseMsat, FeeProportionalMillionths: node2.FeeRate, ToNode: node1Vertex, + InboundFee: getInboundFees(node2), //nolint:ll ExtraOpaqueData: getExtraData(node2), } if err := graph.UpdateEdgePolicy(edgePolicy); err != nil { diff --git a/rpcserver.go b/rpcserver.go index 79d91e6466c..758400b1f27 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -6776,23 +6776,6 @@ func marshalExtraOpaqueData(data []byte) map[uint64][]byte { return records } -// extractInboundFeeSafe tries to extract the inbound fee from the given extra -// opaque data tlv block. If parsing fails, a zero inbound fee is returned. This -// function is typically used on unvalidated data coming stored in the database. -// There is not much we can do other than ignoring errors here. -func extractInboundFeeSafe(data lnwire.ExtraOpaqueData) lnwire.Fee { - var inboundFee lnwire.Fee - - _, err := data.ExtractRecords(&inboundFee) - if err != nil { - // Return zero fee. Do not return the inboundFee variable - // because it may be undefined. - return lnwire.Fee{} - } - - return inboundFee -} - func marshalDBEdge(edgeInfo *models.ChannelEdgeInfo, c1, c2 *models.ChannelEdgePolicy) *lnrpc.ChannelEdge { @@ -6842,7 +6825,7 @@ func marshalDBRoutingPolicy( disabled := policy.ChannelFlags&lnwire.ChanUpdateDisabled != 0 customRecords := marshalExtraOpaqueData(policy.ExtraOpaqueData) - inboundFee := extractInboundFeeSafe(policy.ExtraOpaqueData) + inboundFee := policy.InboundFee.UnwrapOr(lnwire.Fee{}) return &lnrpc.RoutingPolicy{ TimeLockDelta: uint32(policy.TimeLockDelta), @@ -7349,9 +7332,7 @@ func marshallTopologyChange( customRecords := marshalExtraOpaqueData( channelUpdate.ExtraOpaqueData, ) - inboundFee := extractInboundFeeSafe( - channelUpdate.ExtraOpaqueData, - ) + inboundFee := channelUpdate.InboundFee.UnwrapOr(lnwire.Fee{}) channelUpdates[i] = &lnrpc.ChannelEdgeUpdate{ ChanId: channelUpdate.ChanID, @@ -7698,14 +7679,9 @@ func (r *rpcServer) FeeReport(ctx context.Context, edgePolicy.FeeProportionalMillionths feeRate := float64(feeRateFixedPoint) / feeBase - // Decode inbound fee from extra data. - var inboundFee lnwire.Fee - _, err := edgePolicy.ExtraOpaqueData.ExtractRecords( - &inboundFee, + inboundFee := edgePolicy.InboundFee.UnwrapOr( + lnwire.Fee{}, ) - if err != nil { - return err - } // TODO(roasbeef): also add stats for revenue for each // channel diff --git a/tlv/internal/gen/gen_tlv_types.go b/tlv/internal/gen/gen_tlv_types.go index ab9bbbbd66b..9f15e3b6932 100644 --- a/tlv/internal/gen/gen_tlv_types.go +++ b/tlv/internal/gen/gen_tlv_types.go @@ -22,6 +22,10 @@ const ( // second defined unsigned TLV range used in pure TLV messages. pureTLVSecondUnsignedTypeRangeStart uint32 = 3000000000 + // inboundFeeType defines the TLV type used within the channel_update + // message to indicate the inbound fee for a channel. + inboundFeeType = 55555 + defaultOutputFile = "tlv_types_generated.go" ) @@ -29,6 +33,7 @@ const ( var typeMarkers = map[uint32]struct{}{ pureTLVSecondSignedTypeRangeStart: {}, pureTLVSecondUnsignedTypeRangeStart: {}, + inboundFeeType: {}, } const typeCodeTemplate = `// Code generated by tlv/internal/gen; DO NOT EDIT. diff --git a/tlv/tlv_types_generated.go b/tlv/tlv_types_generated.go index 9a80f9364e3..f0eefc4a55d 100644 --- a/tlv/tlv_types_generated.go +++ b/tlv/tlv_types_generated.go @@ -3012,6 +3012,16 @@ func (t *tlvType300) tlv() {} type TlvType300 = *tlvType300 +type tlvType55555 struct{} + +func (t *tlvType55555) TypeVal() Type { + return 55555 +} + +func (t *tlvType55555) tlv() {} + +type TlvType55555 = *tlvType55555 + type tlvType65536 struct{} func (t *tlvType65536) TypeVal() Type {