Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions discovery/gossiper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down
8 changes: 7 additions & 1 deletion docs/release-notes/release-notes-0.20.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
7 changes: 5 additions & 2 deletions graph/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions graph/db/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
19 changes: 6 additions & 13 deletions graph/db/graph_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions graph/db/graph_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
},
Expand Down
18 changes: 12 additions & 6 deletions graph/db/graph_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 60 additions & 22 deletions graph/db/kv_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -474,24 +482,19 @@ 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,
OtherNode: e.NodeKey2Bytes,
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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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))
}
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Q: do we plan to do anything with this error? Right now we return it and then match everywhere if it's ErrParsingExtraTLVBytes to ignore it. Maybe we could use the same approach we already do in rpcserver.go's extractInboundFeeSafe(data lnwire.ExtraOpaqueData) lnwire.Fee and ignore it right here at parsing time? wdyt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the thing that is nice about returning it is then we skip the whole edge rather than just leaving it as &lnwire.Fee{} and still making use of the edge.

leaving it as is lets us not load these invalid policies.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

(gonna request override merge in the mean time - happy to address action items from this discussion in a follow up)

}

val, ok := typeMap[lnwire.FeeRecordType]
if ok && val == nil {
edge.InboundFee = fn.Some(inboundFee)
}

return edge, nil
}

Expand Down
10 changes: 10 additions & 0 deletions graph/db/models/channel_edge_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

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

Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions graph/db/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
}

Expand Down
Loading
Loading