Merge pull request #9897 from ellemouton/inboundFeeTLV

multi: explicitly define InboundFees in ChannelUpdate and ChannelEdgePolicy
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2025-06-10 11:05:48 -07:00
commit 32592dbd20
22 files changed
+248 -101

No files matched your search

+1
View File
@@ -3257,6 +3257,7 @@ func (d *AuthenticatedGossiper) handleChanUpdate(ctx context.Context,
MaxHTLC: upd.HtlcMaximumMsat, MaxHTLC: upd.HtlcMaximumMsat,
FeeBaseMSat: lnwire.MilliSatoshi(upd.BaseFee), FeeBaseMSat: lnwire.MilliSatoshi(upd.BaseFee),
FeeProportionalMillionths: lnwire.MilliSatoshi(upd.FeeRate), FeeProportionalMillionths: lnwire.MilliSatoshi(upd.FeeRate),
InboundFee: upd.InboundFee.ValOpt(),
ExtraOpaqueData: upd.ExtraOpaqueData, ExtraOpaqueData: upd.ExtraOpaqueData,
} }
+7 -1
View File
@@ -88,6 +88,11 @@ circuit. The indices are only available for forwarding events saved after v0.20.
# Technical and Architectural Updates # Technical and Architectural Updates
## BOLT Spec 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 ## Testing
## Database ## Database
@@ -99,4 +104,5 @@ circuit. The indices are only available for forwarding events saved after v0.20.
# Contributors (Alphabetical Order) # Contributors (Alphabetical Order)
* Abdulkbk * Abdulkbk
Pins * Elle Mouton
* Pins
+3
View File
@@ -203,6 +203,9 @@ require (
// store have been included in a tagged sqldb version. // store have been included in a tagged sqldb version.
replace github.com/lightningnetwork/lnd/sqldb => ./sqldb 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 // This replace is for https://github.com/advisories/GHSA-25xm-hr59-7c27
replace github.com/ulikunitz/xz => github.com/ulikunitz/xz v0.5.11 replace github.com/ulikunitz/xz => github.com/ulikunitz/xz v0.5.11
-2
View File
@@ -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/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 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM=
github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA= 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 h1:WHUumk7WgU6BUFsqHuqszI9P6nfhMeIG+rjJBlVE6OE=
github.com/lightningnetwork/lnd/tor v1.1.6/go.mod h1:qSRB8llhAK+a6kaTPWOLLXSZc6Hg8ZC0mq1sUQ/8JfI= 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= github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw=
+5 -2
View File
@@ -943,7 +943,7 @@ func (b *Builder) ApplyChannelUpdate(msg *lnwire.ChannelUpdate1) bool {
return false return false
} }
err = b.UpdateEdge(&models.ChannelEdgePolicy{ update := &models.ChannelEdgePolicy{
SigBytes: msg.Signature.ToSignatureBytes(), SigBytes: msg.Signature.ToSignatureBytes(),
ChannelID: msg.ShortChannelID.ToUint64(), ChannelID: msg.ShortChannelID.ToUint64(),
LastUpdate: time.Unix(int64(msg.Timestamp), 0), LastUpdate: time.Unix(int64(msg.Timestamp), 0),
@@ -954,8 +954,11 @@ func (b *Builder) ApplyChannelUpdate(msg *lnwire.ChannelUpdate1) bool {
MaxHTLC: msg.HtlcMaximumMsat, MaxHTLC: msg.HtlcMaximumMsat,
FeeBaseMSat: lnwire.MilliSatoshi(msg.BaseFee), FeeBaseMSat: lnwire.MilliSatoshi(msg.BaseFee),
FeeProportionalMillionths: lnwire.MilliSatoshi(msg.FeeRate), FeeProportionalMillionths: lnwire.MilliSatoshi(msg.FeeRate),
InboundFee: msg.InboundFee.ValOpt(),
ExtraOpaqueData: msg.ExtraOpaqueData, ExtraOpaqueData: msg.ExtraOpaqueData,
}) }
err = b.UpdateEdge(update)
if err != nil && !IsError(err, ErrIgnored, ErrOutdated) { if err != nil && !IsError(err, ErrIgnored, ErrOutdated) {
log.Errorf("Unable to apply channel update: %v", err) log.Errorf("Unable to apply channel update: %v", err)
return false return false
+4
View File
@@ -12,6 +12,10 @@ var (
ErrEdgePolicyOptionalFieldNotFound = fmt.Errorf("optional field not " + ErrEdgePolicyOptionalFieldNotFound = fmt.Errorf("optional field not " +
"present") "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 // ErrGraphNotFound is returned when at least one of the components of
// graph doesn't exist. // graph doesn't exist.
ErrGraphNotFound = fmt.Errorf("graph bucket not initialized") ErrGraphNotFound = fmt.Errorf("graph bucket not initialized")
+6 -13
View File
@@ -180,17 +180,6 @@ func (c *GraphCache) updateOrAddEdge(node route.Vertex, edge *DirectedChannel) {
func (c *GraphCache) UpdatePolicy(policy *models.ChannelEdgePolicy, fromNode, func (c *GraphCache) UpdatePolicy(policy *models.ChannelEdgePolicy, fromNode,
toNode route.Vertex, edge1 bool) { 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() c.mtx.Lock()
defer c.mtx.Unlock() defer c.mtx.Unlock()
@@ -216,13 +205,17 @@ func (c *GraphCache) UpdatePolicy(policy *models.ChannelEdgePolicy, fromNode,
// policy for node 1. // policy for node 1.
case channel.IsNode1 && edge1: case channel.IsNode1 && edge1:
channel.OutPolicySet = true 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 // This is node 2, and it is edge 2, so this is the outgoing
// policy for node 2. // policy for node 2.
case !channel.IsNode1 && !edge1: case !channel.IsNode1 && !edge1:
channel.OutPolicySet = true 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 // The other two cases left mean it's the inbound policy for the
// node. // node.
+7
View File
@@ -4,6 +4,7 @@ import (
"encoding/hex" "encoding/hex"
"testing" "testing"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
@@ -37,11 +38,17 @@ func TestGraphCacheAddNode(t *testing.T) {
channelFlagA, channelFlagB = 1, 0 channelFlagA, channelFlagB = 1, 0
} }
inboundFee := lnwire.Fee{
BaseFee: 10,
FeeRate: 20,
}
outPolicy1 := &models.ChannelEdgePolicy{ outPolicy1 := &models.ChannelEdgePolicy{
ChannelID: 1000, ChannelID: 1000,
ChannelFlags: lnwire.ChanUpdateChanFlags(channelFlagA), ChannelFlags: lnwire.ChanUpdateChanFlags(channelFlagA),
ToNode: nodeB, ToNode: nodeB,
// Define an inbound fee. // Define an inbound fee.
InboundFee: fn.Some(inboundFee),
ExtraOpaqueData: []byte{ ExtraOpaqueData: []byte{
253, 217, 3, 8, 0, 0, 0, 10, 0, 0, 0, 20, 253, 217, 3, 8, 0, 0, 0, 10, 0, 0, 0, 20,
}, },
+12 -6
View File
@@ -4195,19 +4195,25 @@ func TestGraphCacheForEachNodeChannel(t *testing.T) {
directedChan := getSingleChannel() directedChan := getSingleChannel()
require.NotNil(t, directedChan) require.NotNil(t, directedChan)
require.Equal(t, directedChan.InboundFee, lnwire.Fee{ expectedInbound := lnwire.Fee{
BaseFee: 10, BaseFee: 10,
FeeRate: 20, FeeRate: 20,
}) }
require.Equal(t, expectedInbound, directedChan.InboundFee)
// Set an invalid inbound fee and check that the edge is no longer // Set an invalid inbound fee and check that persistence fails.
// returned.
edge1.ExtraOpaqueData = []byte{ edge1.ExtraOpaqueData = []byte{
253, 217, 3, 8, 0, 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 // TestGraphLoading asserts that the cache is properly reconstructed after a
+60 -22
View File
@@ -22,6 +22,7 @@ import (
"github.com/btcsuite/btcwallet/walletdb" "github.com/btcsuite/btcwallet/walletdb"
"github.com/lightningnetwork/lnd/aliasmgr" "github.com/lightningnetwork/lnd/aliasmgr"
"github.com/lightningnetwork/lnd/batch" "github.com/lightningnetwork/lnd/batch"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/kvdb"
@@ -284,6 +285,13 @@ func (c *KVStore) getChannelMap(edges kvdb.RBucket) (
case errors.Is(err, ErrEdgePolicyOptionalFieldNotFound): case errors.Is(err, ErrEdgePolicyOptionalFieldNotFound):
return nil 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: case err != nil:
return err return err
} }
@@ -474,16 +482,6 @@ func (c *KVStore) forEachNodeDirectedChannel(tx kvdb.RTx,
cachedInPolicy.ToNodeFeatures = toNodeFeatures 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{ directedChannel := &DirectedChannel{
ChannelID: e.ChannelID, ChannelID: e.ChannelID,
IsNode1: node == e.NodeKey1Bytes, IsNode1: node == e.NodeKey1Bytes,
@@ -491,7 +489,12 @@ func (c *KVStore) forEachNodeDirectedChannel(tx kvdb.RTx,
Capacity: e.Capacity, Capacity: e.Capacity,
OutPolicySet: p1 != nil, OutPolicySet: p1 != nil,
InPolicy: cachedInPolicy, InPolicy: cachedInPolicy,
InboundFee: inboundFee, }
if p1 != nil {
p1.InboundFee.WhenSome(func(fee lnwire.Fee) {
directedChannel.InboundFee = fee
})
} }
if node == e.NodeKey2Bytes { if node == e.NodeKey2Bytes {
@@ -2342,7 +2345,7 @@ func (c *KVStore) FilterChannelRange(startHeight,
edge, err := deserializeChanEdgePolicyRaw(r) edge, err := deserializeChanEdgePolicyRaw(r)
if err != nil && !errors.Is( if err != nil && !errors.Is(
err, ErrEdgePolicyOptionalFieldNotFound, err, ErrEdgePolicyOptionalFieldNotFound,
) { ) && !errors.Is(err, ErrParsingExtraTLVBytes) {
return err return err
} }
@@ -2357,7 +2360,7 @@ func (c *KVStore) FilterChannelRange(startHeight,
edge, err := deserializeChanEdgePolicyRaw(r) edge, err := deserializeChanEdgePolicyRaw(r)
if err != nil && !errors.Is( if err != nil && !errors.Is(
err, ErrEdgePolicyOptionalFieldNotFound, err, ErrEdgePolicyOptionalFieldNotFound,
) { ) && !errors.Is(err, ErrParsingExtraTLVBytes) {
return err return err
} }
@@ -4334,15 +4337,20 @@ func putChanEdgePolicy(edges kvdb.RwBucket, edge *models.ChannelEdgePolicy,
// need to deserialize the existing policy within the database // need to deserialize the existing policy within the database
// (now outdated by the new one), and delete its corresponding // (now outdated by the new one), and delete its corresponding
// entry within the update index. We'll ignore any // entry within the update index. We'll ignore any
// ErrEdgePolicyOptionalFieldNotFound error, as we only need // ErrEdgePolicyOptionalFieldNotFound or ErrParsingExtraTLVBytes
// the channel ID and update time to delete the entry. // 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 // TODO(halseth): get rid of these invalid policies in a
// migration. // migration.
// TODO(elle): complete the above TODO in migration from kvdb
// to SQL.
oldEdgePolicy, err := deserializeChanEdgePolicy( oldEdgePolicy, err := deserializeChanEdgePolicy(
bytes.NewReader(edgeBytes), bytes.NewReader(edgeBytes),
) )
if err != nil && if err != nil &&
!errors.Is(err, ErrEdgePolicyOptionalFieldNotFound) { !errors.Is(err, ErrEdgePolicyOptionalFieldNotFound) &&
!errors.Is(err, ErrParsingExtraTLVBytes) {
return err return err
} }
@@ -4449,6 +4457,11 @@ func fetchChanEdgePolicy(edges kvdb.RBucket, chanID []byte,
case errors.Is(err, ErrEdgePolicyOptionalFieldNotFound): case errors.Is(err, ErrEdgePolicyOptionalFieldNotFound):
return nil, nil 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: case err != nil:
return nil, err 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 { if len(edge.ExtraOpaqueData) > MaxAllowedExtraOpaqueBytes {
return ErrTooManyExtraOpaqueBytes(len(edge.ExtraOpaqueData)) 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) { func deserializeChanEdgePolicy(r io.Reader) (*models.ChannelEdgePolicy, error) {
// Deserialize the policy. Note that in case an optional field is not // Deserialize the policy. Note that in case an optional field is not
// found, both an error and a populated policy object are returned. // found or if the edge has invalid TLV data, then both an error and a
edge, deserializeErr := deserializeChanEdgePolicyRaw(r) // populated policy object are returned so that the caller can decide
if deserializeErr != nil && // if it still wants to use the edge or not.
!errors.Is(deserializeErr, ErrEdgePolicyOptionalFieldNotFound) { 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, func deserializeChanEdgePolicyRaw(r io.Reader) (*models.ChannelEdgePolicy,
@@ -4657,6 +4679,22 @@ func deserializeChanEdgePolicyRaw(r io.Reader) (*models.ChannelEdgePolicy,
edge.ExtraOpaqueData = opq[8:] 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 return edge, nil
} }
+10
View File
@@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/btcsuite/btcd/btcec/v2/ecdsa" "github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
) )
@@ -65,6 +66,15 @@ type ChannelEdgePolicy struct {
// to. Using this pub key, the channel graph can further be traversed. // to. Using this pub key, the channel graph can further be traversed.
ToNode [33]byte 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 // ExtraOpaqueData is the set of data that was appended to this
// message, some of which we may not actually know how to iterate or // 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 // parse. By holding onto this data, we ensure that we're able to
+5
View File
@@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/graph/db/models" "github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnutils"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@@ -360,6 +361,9 @@ type ChannelEdgeUpdate struct {
// payments. // payments.
Disabled bool 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 // ExtraOpaqueData is the set of data that was appended to this message
// to fill out the full maximum transport message size. These fields can // to fill out the full maximum transport message size. These fields can
// be used to specify optional data such as custom TLV fields. // be used to specify optional data such as custom TLV fields.
@@ -442,6 +446,7 @@ func (c *ChannelGraph) addToTopologyChange(update *TopologyChange,
AdvertisingNode: aNode, AdvertisingNode: aNode,
ConnectingNode: cNode, ConnectingNode: cNode,
Disabled: m.ChannelFlags&lnwire.ChanUpdateDisabled != 0, Disabled: m.ChannelFlags&lnwire.ChanUpdateDisabled != 0,
InboundFee: m.InboundFee,
ExtraOpaqueData: m.ExtraOpaqueData, ExtraOpaqueData: m.ExtraOpaqueData,
} }
+1
View File
@@ -121,6 +121,7 @@ func randEdgePolicy(chanID *lnwire.ShortChannelID,
FeeBaseMSat: lnwire.MilliSatoshi(prand.Int31()), FeeBaseMSat: lnwire.MilliSatoshi(prand.Int31()),
FeeProportionalMillionths: lnwire.MilliSatoshi(prand.Int31()), FeeProportionalMillionths: lnwire.MilliSatoshi(prand.Int31()),
ToNode: node.PubKeyBytes, ToNode: node.PubKeyBytes,
InboundFee: fn.Some(inboundFee),
ExtraOpaqueData: extraOpaqueData, ExtraOpaqueData: extraOpaqueData,
}, nil }, nil
} }
+32 -2
View File
@@ -6,6 +6,7 @@ import (
"io" "io"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightningnetwork/lnd/tlv"
) )
// ChanUpdateMsgFlags is a bitfield that signals whether optional fields are // 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 is the maximum HTLC value which will be accepted.
HtlcMaximumMsat MilliSatoshi 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 // ExtraData is the set of data that was appended to this message to
// fill out the full maximum transport message size. These fields can // fill out the full maximum transport message size. These fields can
// be used to specify optional data such as custom TLV fields. // 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 { if err != nil {
return err 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 // 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. // Finally, append any extra opaque data.
return WriteBytes(w, a.ExtraOpaqueData) return WriteBytes(w, a.ExtraOpaqueData)
} }
+42 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/fn/v2"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/tlv" "github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
"pgregory.net/rapid" "pgregory.net/rapid"
) )
@@ -406,6 +407,45 @@ func (a *ChannelUpdate1) RandTestMessage(t *rapid.T) Message {
maxHtlc = 0 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{ return &ChannelUpdate1{
Signature: RandSignature(t), Signature: RandSignature(t),
ChainHash: hash, ChainHash: hash,
@@ -428,7 +468,8 @@ func (a *ChannelUpdate1) RandTestMessage(t *rapid.T) Message {
t, "feeRate"), t, "feeRate"),
), ),
HtlcMaximumMsat: maxHtlc, HtlcMaximumMsat: maxHtlc,
ExtraOpaqueData: RandExtraOpaqueData(t, nil), InboundFee: inboundFee,
ExtraOpaqueData: extraBytes,
} }
} }
+9 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@@ -138,7 +139,7 @@ func ExtractChannelUpdate(ownerPubKey []byte,
func UnsignedChannelUpdateFromEdge(info *models.ChannelEdgeInfo, func UnsignedChannelUpdateFromEdge(info *models.ChannelEdgeInfo,
policy *models.ChannelEdgePolicy) *lnwire.ChannelUpdate1 { policy *models.ChannelEdgePolicy) *lnwire.ChannelUpdate1 {
return &lnwire.ChannelUpdate1{ update := &lnwire.ChannelUpdate1{
ChainHash: info.ChainHash, ChainHash: info.ChainHash,
ShortChannelID: lnwire.NewShortChanIDFromInt(policy.ChannelID), ShortChannelID: lnwire.NewShortChanIDFromInt(policy.ChannelID),
Timestamp: uint32(policy.LastUpdate.Unix()), Timestamp: uint32(policy.LastUpdate.Unix()),
@@ -151,6 +152,13 @@ func UnsignedChannelUpdateFromEdge(info *models.ChannelEdgeInfo,
FeeRate: uint32(policy.FeeProportionalMillionths), FeeRate: uint32(policy.FeeProportionalMillionths),
ExtraOpaqueData: policy.ExtraOpaqueData, 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 // ChannelUpdateFromEdge reconstructs a signed ChannelUpdate from the given edge
+4 -14
View File
@@ -1205,27 +1205,17 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) (
// routing policy into a forwarding policy. // routing policy into a forwarding policy.
var forwardingPolicy *models.ForwardingPolicy var forwardingPolicy *models.ForwardingPolicy
if selfPolicy != nil { 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{ forwardingPolicy = &models.ForwardingPolicy{
MinHTLCOut: selfPolicy.MinHTLC, MinHTLCOut: selfPolicy.MinHTLC,
MaxHTLC: selfPolicy.MaxHTLC, MaxHTLC: selfPolicy.MaxHTLC,
BaseFee: selfPolicy.FeeBaseMSat, BaseFee: selfPolicy.FeeBaseMSat,
FeeRate: selfPolicy.FeeProportionalMillionths, FeeRate: selfPolicy.FeeProportionalMillionths,
TimeLockDelta: uint32(selfPolicy.TimeLockDelta), TimeLockDelta: uint32(selfPolicy.TimeLockDelta),
InboundFee: inboundFee,
} }
selfPolicy.InboundFee.WhenSome(func(fee lnwire.Fee) {
inboundFee := models.NewInboundFeeFromWire(fee)
forwardingPolicy.InboundFee = inboundFee
})
} else { } else {
p.log.Warnf("Unable to find our forwarding policy "+ p.log.Warnf("Unable to find our forwarding policy "+
"for channel %v, using default values", "for channel %v, using default values",
+5 -5
View File
@@ -127,12 +127,10 @@ func (r *Manager) UpdatePolicy(newSchema routing.ChannelPolicy,
Edge: edge, Edge: edge,
}) })
// Extract inbound fees from the ExtraOpaqueData.
var inboundWireFee lnwire.Fee var inboundWireFee lnwire.Fee
_, err = edge.ExtraOpaqueData.ExtractRecords(&inboundWireFee) edge.InboundFee.WhenSome(func(fee lnwire.Fee) {
if err != nil { inboundWireFee = fee
return err })
}
inboundFee := models.NewInboundFeeFromWire(inboundWireFee) inboundFee := models.NewInboundFeeFromWire(inboundWireFee)
// Add updated policy to list of policies to send to switch. // 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, err = fn.MapOptionZ(newSchema.InboundFee,
func(f models.InboundFee) error { func(f models.InboundFee) error {
inboundWireFee := f.ToWire() inboundWireFee := f.ToWire()
edge.InboundFee = fn.Some(inboundWireFee)
return edge.ExtraOpaqueData.PackRecords( return edge.ExtraOpaqueData.PackRecords(
&inboundWireFee, &inboundWireFee,
) )
+16 -4
View File
@@ -666,15 +666,25 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
return nil, err return nil, err
} }
getExtraData := func( getInboundFees := func(
end *testChannelEnd) lnwire.ExtraOpaqueData { end *testChannelEnd) fn.Option[lnwire.Fee] {
var extraData lnwire.ExtraOpaqueData
inboundFee := lnwire.Fee{ inboundFee := lnwire.Fee{
BaseFee: int32(end.InboundFeeBaseMsat), BaseFee: int32(end.InboundFeeBaseMsat),
FeeRate: int32(end.InboundFeeRate), 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 return extraData
} }
@@ -701,6 +711,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
FeeBaseMSat: node1.FeeBaseMsat, FeeBaseMSat: node1.FeeBaseMsat,
FeeProportionalMillionths: node1.FeeRate, FeeProportionalMillionths: node1.FeeRate,
ToNode: node2Vertex, ToNode: node2Vertex,
InboundFee: getInboundFees(node1), //nolint:ll
ExtraOpaqueData: getExtraData(node1), ExtraOpaqueData: getExtraData(node1),
} }
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil { if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
@@ -731,6 +742,7 @@ func createTestGraphFromChannels(t *testing.T, useCache bool,
FeeBaseMSat: node2.FeeBaseMsat, FeeBaseMSat: node2.FeeBaseMsat,
FeeProportionalMillionths: node2.FeeRate, FeeProportionalMillionths: node2.FeeRate,
ToNode: node1Vertex, ToNode: node1Vertex,
InboundFee: getInboundFees(node2), //nolint:ll
ExtraOpaqueData: getExtraData(node2), ExtraOpaqueData: getExtraData(node2),
} }
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil { if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
+4 -28
View File
@@ -6776,23 +6776,6 @@ func marshalExtraOpaqueData(data []byte) map[uint64][]byte {
return records 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, func marshalDBEdge(edgeInfo *models.ChannelEdgeInfo,
c1, c2 *models.ChannelEdgePolicy) *lnrpc.ChannelEdge { c1, c2 *models.ChannelEdgePolicy) *lnrpc.ChannelEdge {
@@ -6842,7 +6825,7 @@ func marshalDBRoutingPolicy(
disabled := policy.ChannelFlags&lnwire.ChanUpdateDisabled != 0 disabled := policy.ChannelFlags&lnwire.ChanUpdateDisabled != 0
customRecords := marshalExtraOpaqueData(policy.ExtraOpaqueData) customRecords := marshalExtraOpaqueData(policy.ExtraOpaqueData)
inboundFee := extractInboundFeeSafe(policy.ExtraOpaqueData) inboundFee := policy.InboundFee.UnwrapOr(lnwire.Fee{})
return &lnrpc.RoutingPolicy{ return &lnrpc.RoutingPolicy{
TimeLockDelta: uint32(policy.TimeLockDelta), TimeLockDelta: uint32(policy.TimeLockDelta),
@@ -7349,9 +7332,7 @@ func marshallTopologyChange(
customRecords := marshalExtraOpaqueData( customRecords := marshalExtraOpaqueData(
channelUpdate.ExtraOpaqueData, channelUpdate.ExtraOpaqueData,
) )
inboundFee := extractInboundFeeSafe( inboundFee := channelUpdate.InboundFee.UnwrapOr(lnwire.Fee{})
channelUpdate.ExtraOpaqueData,
)
channelUpdates[i] = &lnrpc.ChannelEdgeUpdate{ channelUpdates[i] = &lnrpc.ChannelEdgeUpdate{
ChanId: channelUpdate.ChanID, ChanId: channelUpdate.ChanID,
@@ -7698,14 +7679,9 @@ func (r *rpcServer) FeeReport(ctx context.Context,
edgePolicy.FeeProportionalMillionths edgePolicy.FeeProportionalMillionths
feeRate := float64(feeRateFixedPoint) / feeBase feeRate := float64(feeRateFixedPoint) / feeBase
// Decode inbound fee from extra data. inboundFee := edgePolicy.InboundFee.UnwrapOr(
var inboundFee lnwire.Fee lnwire.Fee{},
_, err := edgePolicy.ExtraOpaqueData.ExtractRecords(
&inboundFee,
) )
if err != nil {
return err
}
// TODO(roasbeef): also add stats for revenue for each // TODO(roasbeef): also add stats for revenue for each
// channel // channel
+5
View File
@@ -22,6 +22,10 @@ const (
// second defined unsigned TLV range used in pure TLV messages. // second defined unsigned TLV range used in pure TLV messages.
pureTLVSecondUnsignedTypeRangeStart uint32 = 3000000000 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" defaultOutputFile = "tlv_types_generated.go"
) )
@@ -29,6 +33,7 @@ const (
var typeMarkers = map[uint32]struct{}{ var typeMarkers = map[uint32]struct{}{
pureTLVSecondSignedTypeRangeStart: {}, pureTLVSecondSignedTypeRangeStart: {},
pureTLVSecondUnsignedTypeRangeStart: {}, pureTLVSecondUnsignedTypeRangeStart: {},
inboundFeeType: {},
} }
const typeCodeTemplate = `// Code generated by tlv/internal/gen; DO NOT EDIT. const typeCodeTemplate = `// Code generated by tlv/internal/gen; DO NOT EDIT.
+10
View File
@@ -3012,6 +3012,16 @@ func (t *tlvType300) tlv() {}
type TlvType300 = *tlvType300 type TlvType300 = *tlvType300
type tlvType55555 struct{}
func (t *tlvType55555) TypeVal() Type {
return 55555
}
func (t *tlvType55555) tlv() {}
type TlvType55555 = *tlvType55555
type tlvType65536 struct{} type tlvType65536 struct{}
func (t *tlvType65536) TypeVal() Type { func (t *tlvType65536) TypeVal() Type {