routing+migration32: update MC encoding to use pure TLV

In this commit, we update an existing migration which at the time of
writing has not been included in a release. We update it so that it
converts the format used for MissionControl result encoding to use pure
TLV instead. The 3 structs that have been updated are: `mcHop`,
`mcRoute` and `paymentResult`.
This commit is contained in:
Elle Mouton
2024-10-08 13:43:30 +02:00
parent d9a073ad7e
commit 5370c90906
7 changed files with 1612 additions and 690 deletions

View File

@@ -1,11 +1,15 @@
package routing
import (
"bytes"
"fmt"
"io"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/tlv"
)
// Instantiate variables to allow taking a reference from the failure reason.
@@ -76,63 +80,73 @@ type interpretedResult struct {
// interpretResult interprets a payment outcome and returns an object that
// contains information required to update mission control.
func interpretResult(rt *mcRoute, success bool, failureSrcIdx *int,
failure lnwire.FailureMessage) *interpretedResult {
func interpretResult(rt *mcRoute,
failure fn.Option[paymentFailure]) *interpretedResult {
i := &interpretedResult{
pairResults: make(map[DirectedNodePair]pairResult),
}
if success {
return fn.ElimOption(failure, func() *interpretedResult {
i.processSuccess(rt)
} else {
i.processFail(rt, failureSrcIdx, failure)
}
return i
return i
}, func(info paymentFailure) *interpretedResult {
i.processFail(rt, info)
return i
})
}
// processSuccess processes a successful payment attempt.
func (i *interpretedResult) processSuccess(route *mcRoute) {
// For successes, all nodes must have acted in the right way. Therefore
// we mark all of them with a success result.
i.successPairRange(route, 0, len(route.hops)-1)
i.successPairRange(route, 0, len(route.hops.Val)-1)
}
// processFail processes a failed payment attempt.
func (i *interpretedResult) processFail(rt *mcRoute, errSourceIdx *int,
failure lnwire.FailureMessage) {
if errSourceIdx == nil {
func (i *interpretedResult) processFail(rt *mcRoute, failure paymentFailure) {
if failure.info.IsNone() {
i.processPaymentOutcomeUnknown(rt)
return
}
var (
idx int
failMsg lnwire.FailureMessage
)
failure.info.WhenSome(
func(r tlv.RecordT[tlv.TlvType0, paymentFailureInfo]) {
idx = int(r.Val.sourceIdx.Val)
failMsg = r.Val.msg.Val.FailureMessage
},
)
// If the payment was to a blinded route and we received an error from
// after the introduction point, handle this error separately - there
// has been a protocol violation from the introduction node. This
// penalty applies regardless of the error code that is returned.
introIdx, isBlinded := introductionPointIndex(rt)
if isBlinded && introIdx < *errSourceIdx {
i.processPaymentOutcomeBadIntro(rt, introIdx, *errSourceIdx)
if isBlinded && introIdx < idx {
i.processPaymentOutcomeBadIntro(rt, introIdx, idx)
return
}
switch *errSourceIdx {
switch idx {
// We are the source of the failure.
case 0:
i.processPaymentOutcomeSelf(rt, failure)
i.processPaymentOutcomeSelf(rt, failMsg)
// A failure from the final hop was received.
case len(rt.hops):
i.processPaymentOutcomeFinal(rt, failure)
case len(rt.hops.Val):
i.processPaymentOutcomeFinal(rt, failMsg)
// An intermediate hop failed. Interpret the outcome, update reputation
// and try again.
default:
i.processPaymentOutcomeIntermediate(
rt, *errSourceIdx, failure,
)
i.processPaymentOutcomeIntermediate(rt, idx, failMsg)
}
}
@@ -158,7 +172,7 @@ func (i *interpretedResult) processPaymentOutcomeBadIntro(route *mcRoute,
// a final failure reason because the recipient can't process the
// payment (independent of the introduction failing to convert the
// error, we can't complete the payment if the last hop fails).
if errSourceIdx == len(route.hops) {
if errSourceIdx == len(route.hops.Val) {
i.finalFailureReason = &reasonError
}
}
@@ -178,7 +192,7 @@ func (i *interpretedResult) processPaymentOutcomeSelf(rt *mcRoute,
i.failNode(rt, 1)
// If this was a payment to a direct peer, we can stop trying.
if len(rt.hops) == 1 {
if len(rt.hops.Val) == 1 {
i.finalFailureReason = &reasonError
}
@@ -188,7 +202,7 @@ func (i *interpretedResult) processPaymentOutcomeSelf(rt *mcRoute,
// available in the link has been updated.
default:
log.Warnf("Routing failure for local channel %v occurred",
rt.hops[0].channelID)
rt.hops.Val[0].channelID)
}
}
@@ -196,7 +210,7 @@ func (i *interpretedResult) processPaymentOutcomeSelf(rt *mcRoute,
func (i *interpretedResult) processPaymentOutcomeFinal(route *mcRoute,
failure lnwire.FailureMessage) {
n := len(route.hops)
n := len(route.hops.Val)
failNode := func() {
i.failNode(route, n)
@@ -396,8 +410,8 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(route *mcRoute,
// Set the node pair for which a channel update may be out of
// date. The second chance logic uses the policyFailure field.
i.policyFailure = &DirectedNodePair{
From: route.hops[errorSourceIdx-1].pubKeyBytes,
To: route.hops[errorSourceIdx].pubKeyBytes,
From: route.hops.Val[errorSourceIdx-1].pubKeyBytes.Val,
To: route.hops.Val[errorSourceIdx].pubKeyBytes.Val,
}
reportOutgoing()
@@ -425,8 +439,8 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(route *mcRoute,
// Set the node pair for which a channel update may be out of
// date. The second chance logic uses the policyFailure field.
i.policyFailure = &DirectedNodePair{
From: route.hops[errorSourceIdx-1].pubKeyBytes,
To: route.hops[errorSourceIdx].pubKeyBytes,
From: route.hops.Val[errorSourceIdx-1].pubKeyBytes.Val,
To: route.hops.Val[errorSourceIdx].pubKeyBytes.Val,
}
// We report incoming channel. If a second pair is granted in
@@ -500,14 +514,14 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(route *mcRoute,
// Note that if LND is extended to support multiple blinded
// routes, this will terminate the payment without re-trying
// the other routes.
if introIdx == len(route.hops)-1 {
if introIdx == len(route.hops.Val)-1 {
i.finalFailureReason = &reasonError
} else {
// If there are other hops between the recipient and
// introduction node, then we just penalize the last
// hop in the blinded route to minimize the storage of
// results for ephemeral keys.
i.failPairBalance(route, len(route.hops)-1)
i.failPairBalance(route, len(route.hops.Val)-1)
}
// In all other cases, we penalize the reporting node. These are all
@@ -522,8 +536,8 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(route *mcRoute,
// (i.e., that we consider our own node to be at index zero). A boolean is
// returned to indicate whether the route contains a blinded portion at all.
func introductionPointIndex(route *mcRoute) (int, bool) {
for i, hop := range route.hops {
if hop.hasBlindingPoint {
for i, hop := range route.hops.Val {
if hop.hasBlindingPoint.IsSome() {
return i + 1, true
}
}
@@ -534,7 +548,7 @@ func introductionPointIndex(route *mcRoute) (int, bool) {
// processPaymentOutcomeUnknown processes a payment outcome for which no failure
// message or source is available.
func (i *interpretedResult) processPaymentOutcomeUnknown(route *mcRoute) {
n := len(route.hops)
n := len(route.hops.Val)
// If this is a direct payment, the destination must be at fault.
if n == 1 {
@@ -551,52 +565,204 @@ func (i *interpretedResult) processPaymentOutcomeUnknown(route *mcRoute) {
// extractMCRoute extracts the fields required by MC from the Route struct to
// create the more minimal mcRoute struct.
func extractMCRoute(route *route.Route) *mcRoute {
func extractMCRoute(r *route.Route) *mcRoute {
return &mcRoute{
sourcePubKey: route.SourcePubKey,
totalAmount: route.TotalAmount,
hops: extractMCHops(route.Hops),
sourcePubKey: tlv.NewRecordT[tlv.TlvType0](r.SourcePubKey),
totalAmount: tlv.NewRecordT[tlv.TlvType1](r.TotalAmount),
hops: tlv.NewRecordT[tlv.TlvType2](
extractMCHops(r.Hops),
),
}
}
// extractMCHops extracts the Hop fields that MC actually uses from a slice of
// Hops.
func extractMCHops(hops []*route.Hop) []*mcHop {
mcHops := make([]*mcHop, len(hops))
for i, hop := range hops {
mcHops[i] = extractMCHop(hop)
}
return mcHops
func extractMCHops(hops []*route.Hop) mcHops {
return fn.Map(extractMCHop, hops)
}
// extractMCHop extracts the Hop fields that MC actually uses from a Hop.
func extractMCHop(hop *route.Hop) *mcHop {
return &mcHop{
channelID: hop.ChannelID,
pubKeyBytes: hop.PubKeyBytes,
amtToFwd: hop.AmtToForward,
hasBlindingPoint: hop.BlindingPoint != nil,
hasCustomRecords: len(hop.CustomRecords) > 0,
h := mcHop{
channelID: tlv.NewPrimitiveRecord[tlv.TlvType0](
hop.ChannelID,
),
pubKeyBytes: tlv.NewRecordT[tlv.TlvType1](hop.PubKeyBytes),
amtToFwd: tlv.NewRecordT[tlv.TlvType2](hop.AmtToForward),
}
if hop.BlindingPoint != nil {
h.hasBlindingPoint = tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType3](lnwire.TrueBoolean{}),
)
}
if hop.CustomRecords != nil {
h.hasCustomRecords = tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType4](lnwire.TrueBoolean{}),
)
}
return &h
}
// mcRoute holds the bare minimum info about a payment attempt route that MC
// requires.
type mcRoute struct {
sourcePubKey route.Vertex
totalAmount lnwire.MilliSatoshi
hops []*mcHop
sourcePubKey tlv.RecordT[tlv.TlvType0, route.Vertex]
totalAmount tlv.RecordT[tlv.TlvType1, lnwire.MilliSatoshi]
hops tlv.RecordT[tlv.TlvType2, mcHops]
}
// Record returns a TLV record that can be used to encode/decode an mcRoute
// to/from a TLV stream.
func (r *mcRoute) Record() tlv.Record {
recordSize := func() uint64 {
var (
b bytes.Buffer
buf [8]byte
)
if err := encodeMCRoute(&b, r, &buf); err != nil {
panic(err)
}
return uint64(len(b.Bytes()))
}
return tlv.MakeDynamicRecord(
0, r, recordSize, encodeMCRoute, decodeMCRoute,
)
}
func encodeMCRoute(w io.Writer, val interface{}, _ *[8]byte) error {
if v, ok := val.(*mcRoute); ok {
return serializeRoute(w, v)
}
return tlv.NewTypeForEncodingErr(val, "routing.mcRoute")
}
func decodeMCRoute(r io.Reader, val interface{}, _ *[8]byte, l uint64) error {
if v, ok := val.(*mcRoute); ok {
route, err := deserializeRoute(io.LimitReader(r, int64(l)))
if err != nil {
return err
}
*v = *route
return nil
}
return tlv.NewTypeForDecodingErr(val, "routing.mcRoute", l, l)
}
// mcHops is a list of mcHop records.
type mcHops []*mcHop
// Record returns a TLV record that can be used to encode/decode a list of
// mcHop to/from a TLV stream.
func (h *mcHops) Record() tlv.Record {
recordSize := func() uint64 {
var (
b bytes.Buffer
buf [8]byte
)
if err := encodeMCHops(&b, h, &buf); err != nil {
panic(err)
}
return uint64(len(b.Bytes()))
}
return tlv.MakeDynamicRecord(
0, h, recordSize, encodeMCHops, decodeMCHops,
)
}
func encodeMCHops(w io.Writer, val interface{}, buf *[8]byte) error {
if v, ok := val.(*mcHops); ok {
// Encode the number of hops as a var int.
if err := tlv.WriteVarInt(w, uint64(len(*v)), buf); err != nil {
return err
}
// With that written out, we'll now encode the entries
// themselves as a sub-TLV record, which includes its _own_
// inner length prefix.
for _, hop := range *v {
var hopBytes bytes.Buffer
if err := serializeHop(&hopBytes, hop); err != nil {
return err
}
// We encode the record with a varint length followed by
// the _raw_ TLV bytes.
tlvLen := uint64(len(hopBytes.Bytes()))
if err := tlv.WriteVarInt(w, tlvLen, buf); err != nil {
return err
}
if _, err := w.Write(hopBytes.Bytes()); err != nil {
return err
}
}
return nil
}
return tlv.NewTypeForEncodingErr(val, "routing.mcHops")
}
func decodeMCHops(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
if v, ok := val.(*mcHops); ok {
// First, we'll decode the varint that encodes how many hops
// are encoded in the stream.
numHops, err := tlv.ReadVarInt(r, buf)
if err != nil {
return err
}
// Now that we know how many records we'll need to read, we can
// iterate and read them all out in series.
for i := uint64(0); i < numHops; i++ {
// Read out the varint that encodes the size of this
// inner TLV record.
hopSize, err := tlv.ReadVarInt(r, buf)
if err != nil {
return err
}
// Using this information, we'll create a new limited
// reader that'll return an EOF once the end has been
// reached so the stream stops consuming bytes.
innerTlvReader := &io.LimitedReader{
R: r,
N: int64(hopSize),
}
hop, err := deserializeHop(innerTlvReader)
if err != nil {
return err
}
*v = append(*v, hop)
}
return nil
}
return tlv.NewTypeForDecodingErr(val, "routing.mcHops", l, l)
}
// mcHop holds the bare minimum info about a payment attempt route hop that MC
// requires.
type mcHop struct {
channelID uint64
pubKeyBytes route.Vertex
amtToFwd lnwire.MilliSatoshi
hasBlindingPoint bool
hasCustomRecords bool
channelID tlv.RecordT[tlv.TlvType0, uint64]
pubKeyBytes tlv.RecordT[tlv.TlvType1, route.Vertex]
amtToFwd tlv.RecordT[tlv.TlvType2, lnwire.MilliSatoshi]
hasBlindingPoint tlv.OptionalRecordT[tlv.TlvType3, lnwire.TrueBoolean]
hasCustomRecords tlv.OptionalRecordT[tlv.TlvType4, lnwire.TrueBoolean]
}
// failNode marks the node indicated by idx in the route as failed. It also
@@ -604,7 +770,7 @@ type mcHop struct {
// intentionally panics when the self node is failed.
func (i *interpretedResult) failNode(rt *mcRoute, idx int) {
// Mark the node as failing.
i.nodeFailure = &rt.hops[idx-1].pubKeyBytes
i.nodeFailure = &rt.hops.Val[idx-1].pubKeyBytes.Val
// Mark the incoming connection as failed for the node. We intent to
// penalize as much as we can for a node level failure, including future
@@ -620,7 +786,7 @@ func (i *interpretedResult) failNode(rt *mcRoute, idx int) {
// If not the ultimate node, mark the outgoing connection as failed for
// the node.
if idx < len(rt.hops) {
if idx < len(rt.hops.Val) {
outgoingChannelIdx := idx
outPair, _ := getPair(rt, outgoingChannelIdx)
i.pairResults[outPair] = failPairResult(0)
@@ -667,18 +833,18 @@ func (i *interpretedResult) successPairRange(rt *mcRoute, fromIdx, toIdx int) {
func getPair(rt *mcRoute, channelIdx int) (DirectedNodePair,
lnwire.MilliSatoshi) {
nodeTo := rt.hops[channelIdx].pubKeyBytes
nodeTo := rt.hops.Val[channelIdx].pubKeyBytes.Val
var (
nodeFrom route.Vertex
amt lnwire.MilliSatoshi
)
if channelIdx == 0 {
nodeFrom = rt.sourcePubKey
amt = rt.totalAmount
nodeFrom = rt.sourcePubKey.Val
amt = rt.totalAmount.Val
} else {
nodeFrom = rt.hops[channelIdx-1].pubKeyBytes
amt = rt.hops[channelIdx-1].amtToFwd
nodeFrom = rt.hops.Val[channelIdx-1].pubKeyBytes.Val
amt = rt.hops.Val[channelIdx-1].amtToFwd.Val
}
pair := NewDirectedNodePair(nodeFrom, nodeTo)