Skip to content

Commit

Permalink
htlcswitch: use fat errors as a sender
Browse files Browse the repository at this point in the history
  • Loading branch information
joostjager committed Nov 6, 2023
1 parent cba6e93 commit c16a9f7
Show file tree
Hide file tree
Showing 15 changed files with 186 additions and 16 deletions.
13 changes: 13 additions & 0 deletions channeldb/payments.go
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,12 @@ func serializeHop(w io.Writer, h *route.Hop) error {
records = append(records, record.NewMetadataRecord(&h.Metadata))
}

// Signal attributable errors.
if h.AttrError {
attrError := record.NewAttributableError()
records = append(records, attrError.Record())
}

// Final sanity check to absolutely rule out custom records that are not
// custom and write into the standard range.
if err := h.CustomRecords.Validate(); err != nil {
Expand Down Expand Up @@ -1297,6 +1303,13 @@ func deserializeHop(r io.Reader) (*route.Hop, error) {
h.Metadata = metadata
}

attributableErrorType := uint64(record.AttributableErrorOnionType)
if _, ok := tlvMap[attributableErrorType]; ok {
delete(tlvMap, attributableErrorType)

h.AttrError = true
}

h.CustomRecords = tlvMap

return h, nil
Expand Down
63 changes: 59 additions & 4 deletions htlcswitch/failure.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ package htlcswitch

import (
"bytes"
"encoding/binary"
"fmt"
"strings"

sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/lnwire"
)

var byteOrder = binary.BigEndian

// ClearTextError is an interface which is implemented by errors that occur
// when we know the underlying wire failure message. These errors are the
// opposite to opaque errors which are onion-encrypted blobs only understandable
Expand Down Expand Up @@ -166,7 +170,25 @@ type OnionErrorDecrypter interface {
// SphinxErrorDecrypter wraps the sphinx data SphinxErrorDecrypter and maps the
// returned errors to concrete lnwire.FailureMessage instances.
type SphinxErrorDecrypter struct {
OnionErrorDecrypter
decrypter interface{}
}

// NewSphinxErrorDecrypter instantiates a new error decryptor.
func NewSphinxErrorDecrypter(circuit *sphinx.Circuit,
attrError bool) *SphinxErrorDecrypter {

var decrypter interface{}
if !attrError {
decrypter = sphinx.NewOnionErrorDecrypter(circuit)
} else {
decrypter = sphinx.NewOnionAttrErrorDecrypter(
circuit, hop.AttrErrorStruct,
)
}

return &SphinxErrorDecrypter{
decrypter: decrypter,
}
}

// DecryptError peels off each layer of onion encryption from the first hop, to
Expand All @@ -177,9 +199,42 @@ type SphinxErrorDecrypter struct {
func (s *SphinxErrorDecrypter) DecryptError(reason lnwire.OpaqueReason) (
*ForwardingError, error) {

failure, err := s.OnionErrorDecrypter.DecryptError(reason)
if err != nil {
return nil, err
var failure *sphinx.DecryptedError

switch decrypter := s.decrypter.(type) {
case OnionErrorDecrypter:
legacyError, err := decrypter.DecryptError(reason)
if err != nil {
return nil, err
}

failure = legacyError

case *sphinx.OnionAttrErrorDecrypter:
attributableError, err := decrypter.DecryptError(reason)
if err != nil {
return nil, err
}

// Log hold times.
//
// TODO: Use to penalize nodes.
var holdTimes []string
for _, payload := range attributableError.Payloads {
// Read hold time.
holdTimeMs := byteOrder.Uint32(payload)

holdTimes = append(
holdTimes,
fmt.Sprintf("%v", holdTimeMs),
)
}
log.Debugf("Hold times: %v", strings.Join(holdTimes, "/"))

failure = &attributableError.DecryptedError

default:
panic("unexpected decrypter type")
}

// Decode the failure. If an error occurs, we leave the failure message
Expand Down
2 changes: 1 addition & 1 deletion htlcswitch/failure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestLongFailureMessage(t *testing.T) {
}

errorDecryptor := &SphinxErrorDecrypter{
OnionErrorDecrypter: sphinx.NewOnionErrorDecrypter(circuit),
decrypter: sphinx.NewOnionErrorDecrypter(circuit),
}

// Assert that the failure message can still be extracted.
Expand Down
4 changes: 2 additions & 2 deletions htlcswitch/switch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3253,7 +3253,7 @@ func TestInvalidFailure(t *testing.T) {
// Get payment result from switch. We expect an unreadable failure
// message error.
deobfuscator := SphinxErrorDecrypter{
OnionErrorDecrypter: &mockOnionErrorDecryptor{
decrypter: &mockOnionErrorDecryptor{
err: ErrUnreadableFailureMessage,
},
}
Expand All @@ -3278,7 +3278,7 @@ func TestInvalidFailure(t *testing.T) {
// Modify the decryption to simulate that decryption went alright, but
// the failure cannot be decoded.
deobfuscator = SphinxErrorDecrypter{
OnionErrorDecrypter: &mockOnionErrorDecryptor{
decrypter: &mockOnionErrorDecryptor{
sourceIdx: 2,
message: []byte{200},
},
Expand Down
34 changes: 32 additions & 2 deletions itest/lnd_multi-hop-error-propagation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,53 @@ package itest

import (
"math"
"testing"

"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)

func testHtlcErrorPropagation(ht *lntest.HarnessTest) {
ht.Run("legacy error", func(tt *testing.T) {
st := ht.Subtest(tt)
st.EnsureConnected(st.Alice, st.Bob)

// Test legacy errors using the standby node.
testHtlcErrorPropagationWithNode(st, st.Alice)
})

ht.Run("attr error", func(tt *testing.T) {
st := ht.Subtest(tt)

// Create a different Alice node with attributable
// errors enabled. Alice will signal to Bob and Carol to
// return attributable errors to her.
alice := st.NewNode("Alice", []string{"--routerrpc.attrerrors"})
st.FundCoins(btcutil.SatoshiPerBitcoin, alice)

st.ConnectNodes(alice, st.Bob)

testHtlcErrorPropagationWithNode(st, alice)

st.Shutdown(alice)
})
}

func testHtlcErrorPropagationWithNode(ht *lntest.HarnessTest,
alice *node.HarnessNode) {

// In this test we wish to exercise the daemon's correct parsing,
// handling, and propagation of errors that occur while processing a
// multi-hop payment.
const chanAmt = funding.MaxBtcFundingAmount

alice, bob := ht.Alice, ht.Bob

bob := ht.Bob
// Since we'd like to test some multi-hop failure scenarios, we'll
// introduce another node into our test network: Carol.
carol := ht.NewNode("Carol", nil)
Expand Down
1 change: 1 addition & 0 deletions lnrpc/routerrpc/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,6 @@ func GetRoutingConfig(cfg *Config) *RoutingConfig {
NodeWeight: cfg.BimodalConfig.NodeWeight,
DecayTime: cfg.BimodalConfig.DecayTime,
},
AttrErrors: cfg.AttrErrors,
}
}
4 changes: 4 additions & 0 deletions lnrpc/routerrpc/routing_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ type RoutingConfig struct {

// BimodalConfig defines parameters for the bimodal probability.
BimodalConfig *BimodalConfig `group:"bimodal" namespace:"bimodal" description:"configuration for the bimodal pathfinding probability estimator"`

// AttrErrors indicates whether attributable errors should be requested
// if the whole route supports it.
AttrErrors bool `long:"attrerrors" description:"request attributable errors if the whole route supports it"`
}

// AprioriConfig defines parameters for the apriori probability.
Expand Down
41 changes: 40 additions & 1 deletion routing/pathfind.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/feature"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route"
Expand Down Expand Up @@ -105,6 +106,36 @@ type finalHopParams struct {
metadata []byte
}

// useAttrErrors returns true if the path can use attributable errors.
func useAttrErrors(pathEdges []*channeldb.CachedEdgePolicy) bool {
// Use legacy errors if the route length exceeds the maximum number of
// hops for attributable errors.
if len(pathEdges) > hop.AttrErrorStruct.HopCount() {
return false
}

// Every node along the path must signal support for attributable
// errors.
for _, edge := range pathEdges {
// Get the node features.
toFeat := edge.ToNodeFeatures

// If there are no features known, assume the node cannot handle
// attributable errors.
if toFeat == nil {
return false
}

// If the node does not signal support for attributable errors,
// do not use them.
if !toFeat.HasFeature(lnwire.AttributableErrorsOptional) {
return false
}
}

return true
}

// newRoute constructs a route using the provided path and final hop constraints.
// Any destination specific fields from the final hop params will be attached
// assuming the destination's feature vector signals support, otherwise this
Expand All @@ -117,7 +148,7 @@ type finalHopParams struct {
// dependencies.
func newRoute(sourceVertex route.Vertex,
pathEdges []*channeldb.CachedEdgePolicy, currentHeight uint32,
finalHop finalHopParams) (*route.Route, error) {
finalHop finalHopParams, attrErrors bool) (*route.Route, error) {

var (
hops []*route.Hop
Expand All @@ -134,6 +165,9 @@ func newRoute(sourceVertex route.Vertex,
nextIncomingAmount lnwire.MilliSatoshi
)

// Use attributable errors if enabled and supported by the route.
attributableErrors := attrErrors && useAttrErrors(pathEdges)

pathLength := len(pathEdges)
for i := pathLength - 1; i >= 0; i-- {
// Now we'll start to calculate the items within the per-hop
Expand Down Expand Up @@ -250,6 +284,7 @@ func newRoute(sourceVertex route.Vertex,
CustomRecords: customRecords,
MPP: mpp,
Metadata: metadata,
AttrError: attributableErrors,
}

hops = append([]*route.Hop{currentHop}, hops...)
Expand Down Expand Up @@ -371,6 +406,10 @@ type PathFindingConfig struct {
// MinProbability defines the minimum success probability of the
// returned route.
MinProbability float64

// AttrErrors indicates whether we should use the new attributable
// errors if the nodes on the route allow it.
AttrErrors bool
}

// getOutgoingBalance returns the maximum available balance in any of the
Expand Down
5 changes: 5 additions & 0 deletions routing/pathfind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,7 @@ func runFindLowestFeePath(t *testing.T, useCache bool) {
cltvDelta: finalHopCLTV,
records: nil,
},
false,
)
require.NoError(t, err, "unable to create path")

Expand Down Expand Up @@ -1101,6 +1102,7 @@ func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstanc
cltvDelta: finalHopCLTV,
records: nil,
},
false,
)
require.NoError(t, err, "unable to create path")

Expand Down Expand Up @@ -1639,6 +1641,7 @@ func TestNewRoute(t *testing.T) {
paymentAddr: testCase.paymentAddr,
metadata: testCase.metadata,
},
false,
)

if testCase.expectError {
Expand Down Expand Up @@ -2641,6 +2644,7 @@ func testCltvLimit(t *testing.T, useCache bool, limit uint32,
cltvDelta: finalHopCLTV,
records: nil,
},
false,
)
require.NoError(t, err, "unable to create path")

Expand Down Expand Up @@ -2964,6 +2968,7 @@ func runNoCycle(t *testing.T, useCache bool) {
cltvDelta: finalHopCLTV,
records: nil,
},
false,
)
require.NoError(t, err, "unable to create path")

Expand Down
14 changes: 8 additions & 6 deletions routing/payment_lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (

"github.com/btcsuite/btcd/btcec/v2"
"github.com/davecgh/go-spew/spew"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/lntypes"
Expand Down Expand Up @@ -556,11 +555,14 @@ func (p *shardHandler) collectResult(attempt *channeldb.HTLCAttemptInfo) (
}

// Using the created circuit, initialize the error decrypter so we can
// parse+decode any failures incurred by this payment within the
// switch.
errorDecryptor := &htlcswitch.SphinxErrorDecrypter{
OnionErrorDecrypter: sphinx.NewOnionErrorDecrypter(circuit),
}
// parse+decode any failures incurred by this payment within the switch.
//
// The resolution format to use for the decryption is based on the
// instruction that we gave to the first hop.
attrError := attempt.Route.Hops[0].AttrError
errorDecryptor := htlcswitch.NewSphinxErrorDecrypter(
circuit, attrError,
)

// Now ask the switch to return the result of the payment when
// available.
Expand Down
1 change: 1 addition & 0 deletions routing/payment_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
paymentAddr: p.payment.PaymentAddr,
metadata: p.payment.Metadata,
},
p.pathFindingConfig.AttrErrors,
)
if err != nil {
return nil, err
Expand Down
Loading

0 comments on commit c16a9f7

Please sign in to comment.