diff --git a/channeldb/payments.go b/channeldb/payments.go index 93abe4fb16..387ae968be 100644 --- a/channeldb/payments.go +++ b/channeldb/payments.go @@ -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 { @@ -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 diff --git a/feature/default_sets.go b/feature/default_sets.go index 1b1fd1f104..81cfe94889 100644 --- a/feature/default_sets.go +++ b/feature/default_sets.go @@ -83,4 +83,9 @@ var defaultSetDesc = setDesc{ SetInit: {}, // I SetNodeAnn: {}, // N }, + lnwire.AttributableErrorsOptional: { + SetInit: {}, // I + SetNodeAnn: {}, // N + SetInvoice: {}, // 9 + }, } diff --git a/go.mod b/go.mod index cb83deae38..1c02767e09 100644 --- a/go.mod +++ b/go.mod @@ -206,6 +206,8 @@ replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2 // allows us to specify that as an option. replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display +replace github.com/lightningnetwork/lightning-onion => github.com/joostjager/lightning-onion v0.0.0-20230808121011-787ad3d102b0 + // If you change this please also update .github/pull_request_template.md and // docs/INSTALL.md. go 1.19 diff --git a/go.sum b/go.sum index 0407ae143f..dd74eedaad 100644 --- a/go.sum +++ b/go.sum @@ -376,6 +376,8 @@ github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGAR github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/joostjager/lightning-onion v0.0.0-20230808121011-787ad3d102b0 h1:7WYIggNpUFlFZkcTg3K8EONITymhUnl+B7a6EMsBelI= +github.com/joostjager/lightning-onion v0.0.0-20230808121011-787ad3d102b0/go.mod h1:jXOT3eGidi7oYbmB9LeGZOQLlItWglDVGZwvTcf62Wk= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= @@ -440,8 +442,6 @@ github.com/lightninglabs/neutrino/cache v1.1.1 h1:TllWOSlkABhpgbWJfzsrdUaDH2fBy/ github.com/lightninglabs/neutrino/cache v1.1.1/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo= github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display h1:pRdza2wleRN1L2fJXd6ZoQ9ZegVFTAb2bOQfruJPKcY= github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -github.com/lightningnetwork/lightning-onion v1.2.1-0.20221202012345-ca23184850a1 h1:Wm0g70gkcAu2pGpNZwfWPSVOY21j8IyYsNewwK4OkT4= -github.com/lightningnetwork/lightning-onion v1.2.1-0.20221202012345-ca23184850a1/go.mod h1:7dDx73ApjEZA0kcknI799m2O5kkpfg4/gr7N092ojNo= github.com/lightningnetwork/lnd/cert v1.2.1 h1:CTrTcU0L66J73oqdRLVfNylZyp1Fh97ZezX6IuzkrqE= github.com/lightningnetwork/lnd/cert v1.2.1/go.mod h1:04JhIEodoR6usBN5+XBRtLEEmEHsclLi0tEyxZQNP+w= github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg= diff --git a/htlcswitch/circuit.go b/htlcswitch/circuit.go index efb2a47790..003a86f505 100644 --- a/htlcswitch/circuit.go +++ b/htlcswitch/circuit.go @@ -199,7 +199,7 @@ func (c *PaymentCircuit) Decode(r io.Reader) error { case hop.EncrypterTypeSphinx: // Sphinx encrypter was used as this is a forwarded HTLC. - c.ErrorEncrypter = hop.NewSphinxErrorEncrypter() + c.ErrorEncrypter = hop.NewSphinxErrorEncrypterUninitialized() case hop.EncrypterTypeMock: // Test encrypter. diff --git a/htlcswitch/circuit_map.go b/htlcswitch/circuit_map.go index 9b26a1d07e..0046738748 100644 --- a/htlcswitch/circuit_map.go +++ b/htlcswitch/circuit_map.go @@ -210,9 +210,9 @@ type CircuitMapConfig struct { FetchClosedChannels func( pendingOnly bool) ([]*channeldb.ChannelCloseSummary, error) - // ExtractErrorEncrypter derives the shared secret used to encrypt + // ExtractSharedSecret derives the shared secret used to encrypt // errors from the obfuscator's ephemeral public key. - ExtractErrorEncrypter hop.ErrorEncrypterExtracter + ExtractSharedSecret hop.SharedSecretGenerator // CheckResolutionMsg checks whether a given resolution message exists // for the passed CircuitKey. @@ -633,7 +633,7 @@ func (cm *circuitMap) decodeCircuit(v []byte) (*PaymentCircuit, error) { // Otherwise, we need to reextract the encrypter, so that the shared // secret is rederived from what was decoded. err := circuit.ErrorEncrypter.Reextract( - cm.cfg.ExtractErrorEncrypter, + cm.cfg.ExtractSharedSecret, ) if err != nil { return nil, err diff --git a/htlcswitch/circuit_test.go b/htlcswitch/circuit_test.go index 4fc1e03966..3dfa364684 100644 --- a/htlcswitch/circuit_test.go +++ b/htlcswitch/circuit_test.go @@ -34,6 +34,10 @@ var ( // testExtracter is a precomputed extraction of testEphemeralKey, using // the sphinxPrivKey. testExtracter *hop.SphinxErrorEncrypter + + // testAttributableExtracter is a precomputed extraction of + // testEphemeralKey, using the sphinxPrivKey. + testAttributableExtracter *hop.SphinxErrorEncrypter ) func init() { @@ -66,20 +70,25 @@ func initTestExtracter() { onionProcessor := newOnionProcessor(nil) defer onionProcessor.Stop() - obfuscator, _ := onionProcessor.ExtractErrorEncrypter( + sharedSecret, failCode := onionProcessor.ExtractSharedSecret( testEphemeralKey, ) - - sphinxExtracter, ok := obfuscator.(*hop.SphinxErrorEncrypter) - if !ok { - panic("did not extract sphinx error encrypter") + if failCode != lnwire.CodeNone { + panic("did not extract shared secret") } - testExtracter = sphinxExtracter + testExtracter = hop.NewSphinxErrorEncrypter( + testEphemeralKey, sharedSecret, false, + ) + + testAttributableExtracter = hop.NewSphinxErrorEncrypter( + testEphemeralKey, sharedSecret, true, + ) // We also set this error extracter on startup, otherwise it will be nil // at compile-time. halfCircuitTests[2].encrypter = testExtracter + halfCircuitTests[3].encrypter = testAttributableExtracter } // newOnionProcessor creates starts a new htlcswitch.OnionProcessor using a temp @@ -107,10 +116,10 @@ func newCircuitMap(t *testing.T, resMsg bool) (*htlcswitch.CircuitMapConfig, db := makeCircuitDB(t, "") circuitMapCfg := &htlcswitch.CircuitMapConfig{ - DB: db, - FetchAllOpenChannels: db.ChannelStateDB().FetchAllOpenChannels, - FetchClosedChannels: db.ChannelStateDB().FetchClosedChannels, - ExtractErrorEncrypter: onionProcessor.ExtractErrorEncrypter, + DB: db, + FetchAllOpenChannels: db.ChannelStateDB().FetchAllOpenChannels, + FetchClosedChannels: db.ChannelStateDB().FetchClosedChannels, + ExtractSharedSecret: onionProcessor.ExtractSharedSecret, } if resMsg { @@ -175,6 +184,14 @@ var halfCircuitTests = []struct { // repopulate this encrypter. encrypter: testExtracter, }, + { + hash: hash3, + inValue: 10000, + outValue: 9000, + chanID: lnwire.NewShortChanIDFromInt(3), + htlcID: 3, + encrypter: testAttributableExtracter, + }, } // TestHalfCircuitSerialization checks that the half circuits can be properly @@ -217,7 +234,7 @@ func TestHalfCircuitSerialization(t *testing.T) { // encrypters, this will be a NOP. if circuit2.ErrorEncrypter != nil { err := circuit2.ErrorEncrypter.Reextract( - onionProcessor.ExtractErrorEncrypter, + onionProcessor.ExtractSharedSecret, ) if err != nil { t.Fatalf("unable to reextract sphinx error "+ @@ -646,11 +663,11 @@ func restartCircuitMap(t *testing.T, cfg *htlcswitch.CircuitMapConfig) ( // Reinitialize circuit map with same db path. db := makeCircuitDB(t, dbPath) cfg2 := &htlcswitch.CircuitMapConfig{ - DB: db, - FetchAllOpenChannels: db.ChannelStateDB().FetchAllOpenChannels, - FetchClosedChannels: db.ChannelStateDB().FetchClosedChannels, - ExtractErrorEncrypter: cfg.ExtractErrorEncrypter, - CheckResolutionMsg: cfg.CheckResolutionMsg, + DB: db, + FetchAllOpenChannels: db.ChannelStateDB().FetchAllOpenChannels, + FetchClosedChannels: db.ChannelStateDB().FetchClosedChannels, + ExtractSharedSecret: cfg.ExtractSharedSecret, + CheckResolutionMsg: cfg.CheckResolutionMsg, } cm2, err := htlcswitch.NewCircuitMap(cfg2) require.NoError(t, err, "unable to recreate persistent circuit map") diff --git a/htlcswitch/failure.go b/htlcswitch/failure.go index 373263381f..e51746bc51 100644 --- a/htlcswitch/failure.go +++ b/htlcswitch/failure.go @@ -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 @@ -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 @@ -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 diff --git a/htlcswitch/failure_test.go b/htlcswitch/failure_test.go index 48ebc66821..fe44b47e4f 100644 --- a/htlcswitch/failure_test.go +++ b/htlcswitch/failure_test.go @@ -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. diff --git a/htlcswitch/hop/error_encryptor.go b/htlcswitch/hop/error_encryptor.go index 7b6a3dd1a5..b769a60ac5 100644 --- a/htlcswitch/hop/error_encryptor.go +++ b/htlcswitch/hop/error_encryptor.go @@ -2,14 +2,36 @@ package hop import ( "bytes" + "encoding/binary" + "errors" "fmt" "io" + "time" "github.com/btcsuite/btcd/btcec/v2" sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" ) +const ( + // A set of tlv type definitions used to serialize the encrypter to the + // database. + // + // NOTE: A migration should be added whenever this list changes. This + // prevents against the database being rolled back to an older + // format where the surrounding logic might assume a different set of + // fields are known. + creationTimeType tlv.Type = 0 +) + +// AttrErrorStruct defines the message structure for an attributable error. Use +// a maximum route length of 20, a fixed payload length of 4 bytes to +// accommodate the a 32-bit hold time in milliseconds and use 4 byte hmacs. +// Total size including a 256 byte message from the error source works out to +// 1200 bytes. +var AttrErrorStruct = sphinx.NewAttrErrorStructure(20, 4, 4) + // EncrypterType establishes an enum used in serialization to indicate how to // decode a concrete instance of the ErrorEncrypter interface. type EncrypterType byte @@ -27,9 +49,11 @@ const ( EncrypterTypeMock = 2 ) -// ErrorEncrypterExtracter defines a function signature that extracts an -// ErrorEncrypter from an sphinx OnionPacket. -type ErrorEncrypterExtracter func(*btcec.PublicKey) (ErrorEncrypter, +var byteOrder = binary.BigEndian + +// SharedSecretGenerator defines a function signature that extracts a shared +// secret from an sphinx OnionPacket. +type SharedSecretGenerator func(*btcec.PublicKey) (sphinx.Hash256, lnwire.FailCode) // ErrorEncrypter is an interface that is used to encrypt HTLC related errors @@ -47,12 +71,12 @@ type ErrorEncrypter interface { // message. This method is used when we receive an // UpdateFailMalformedHTLC from the remote peer and then need to // convert that into a proper error from only the raw bytes. - EncryptMalformedError(lnwire.OpaqueReason) lnwire.OpaqueReason + EncryptMalformedError(lnwire.OpaqueReason) (lnwire.OpaqueReason, error) // IntermediateEncrypt wraps an already encrypted opaque reason error // in an additional layer of onion encryption. This process repeats // until the error arrives at the source of the payment. - IntermediateEncrypt(lnwire.OpaqueReason) lnwire.OpaqueReason + IntermediateEncrypt(lnwire.OpaqueReason) (lnwire.OpaqueReason, error) // Type returns an enum indicating the underlying concrete instance // backing this interface. @@ -66,12 +90,13 @@ type ErrorEncrypter interface { // given io.Reader. Decode(io.Reader) error - // Reextract rederives the encrypter using the extracter, performing an - // ECDH with the sphinx router's key and the ephemeral public key. + // Reextract rederives the encrypter using the shared secret generator, + // performing an ECDH with the sphinx router's key and the ephemeral + // public key. // // NOTE: This should be called shortly after Decode to properly // reinitialize the error encrypter. - Reextract(ErrorEncrypterExtracter) error + Reextract(SharedSecretGenerator) error } // SphinxErrorEncrypter is a concrete implementation of both the ErrorEncrypter @@ -79,22 +104,85 @@ type ErrorEncrypter interface { // result, all errors handled are themselves wrapped in layers of onion // encryption and must be treated as such accordingly. type SphinxErrorEncrypter struct { - *sphinx.OnionErrorEncrypter + encrypter interface{} EphemeralKey *btcec.PublicKey + CreatedAt time.Time + + attrError bool } -// NewSphinxErrorEncrypter initializes a blank sphinx error encrypter, that -// should be used to deserialize an encoded SphinxErrorEncrypter. Since the -// actual encrypter is not stored in plaintext while at rest, reconstructing the -// error encrypter requires: +// NewSphinxErrorEncrypterUninitialized initializes a blank sphinx error +// encrypter, that should be used to deserialize an encoded +// SphinxErrorEncrypter. Since the actual encrypter is not stored in plaintext +// while at rest, reconstructing the error encrypter requires: // 1. Decode: to deserialize the ephemeral public key. // 2. Reextract: to "unlock" the actual error encrypter using an active // OnionProcessor. -func NewSphinxErrorEncrypter() *SphinxErrorEncrypter { +func NewSphinxErrorEncrypterUninitialized() *SphinxErrorEncrypter { return &SphinxErrorEncrypter{ - OnionErrorEncrypter: nil, - EphemeralKey: &btcec.PublicKey{}, + EphemeralKey: &btcec.PublicKey{}, + } +} + +// NewSphinxErrorEncrypter creates a new instance of a SphinxErrorEncrypter, +// initialized with the provided shared secret. To deserialize an encoded +// SphinxErrorEncrypter, use the NewSphinxErrorEncrypterUninitialized +// constructor. +func NewSphinxErrorEncrypter(ephemeralKey *btcec.PublicKey, + sharedSecret sphinx.Hash256, + attrError bool) *SphinxErrorEncrypter { + + encrypter := &SphinxErrorEncrypter{ + EphemeralKey: ephemeralKey, + attrError: attrError, + } + + if attrError { + // Set creation time rounded to nanosecond to avoid differences + // after serialization. + encrypter.CreatedAt = time.Now().Truncate(time.Nanosecond) + } + + encrypter.initialize(sharedSecret) + + return encrypter +} + +func (s *SphinxErrorEncrypter) initialize(sharedSecret sphinx.Hash256) { + if s.attrError { + s.encrypter = sphinx.NewOnionAttrErrorEncrypter( + sharedSecret, AttrErrorStruct, + ) + } else { + s.encrypter = sphinx.NewOnionErrorEncrypter( + sharedSecret, + ) + } +} + +func (s *SphinxErrorEncrypter) getHoldTimeMs() uint32 { + return uint32(time.Since(s.CreatedAt).Milliseconds()) +} + +func (s *SphinxErrorEncrypter) encrypt(initial bool, + data []byte) (lnwire.OpaqueReason, error) { + + switch encrypter := s.encrypter.(type) { + case *sphinx.OnionErrorEncrypter: + return encrypter.EncryptError(initial, data), nil + + case *sphinx.OnionAttrErrorEncrypter: + // Pass hold time as the payload. + holdTimeMs := s.getHoldTimeMs() + + var payload [4]byte + byteOrder.PutUint32(payload[:], holdTimeMs) + + return encrypter.EncryptError(initial, data, payload[:]) + + default: + panic("unexpected encrypter type") } } @@ -112,9 +200,7 @@ func (s *SphinxErrorEncrypter) EncryptFirstHop( return nil, err } - // We pass a true as the first parameter to indicate that a MAC should - // be added. - return s.EncryptError(true, b.Bytes()), nil + return s.encrypt(true, b.Bytes()) } // EncryptMalformedError is similar to EncryptFirstHop (it adds the MAC), but @@ -125,9 +211,9 @@ func (s *SphinxErrorEncrypter) EncryptFirstHop( // // NOTE: Part of the ErrorEncrypter interface. func (s *SphinxErrorEncrypter) EncryptMalformedError( - reason lnwire.OpaqueReason) lnwire.OpaqueReason { + reason lnwire.OpaqueReason) (lnwire.OpaqueReason, error) { - return s.EncryptError(true, reason) + return s.encrypt(true, reason) } // IntermediateEncrypt wraps an already encrypted opaque reason error in an @@ -138,9 +224,26 @@ func (s *SphinxErrorEncrypter) EncryptMalformedError( // // NOTE: Part of the ErrorEncrypter interface. func (s *SphinxErrorEncrypter) IntermediateEncrypt( - reason lnwire.OpaqueReason) lnwire.OpaqueReason { + reason lnwire.OpaqueReason) (lnwire.OpaqueReason, error) { + + encrypted, err := s.encrypt(false, reason) + switch { + // If the structure of the error received from downstream is invalid, + // then generate a new failure message with a valid structure so that + // the sender is able to penalize the offending node. + case errors.Is(err, sphinx.ErrInvalidStructure): + // Use an all-zeroes failure message. This is not a defined + // message, but the sender will at least know where the error + // occurred. + reason = make([]byte, lnwire.FailureMessageLength+2+2) + + return s.encrypt(true, reason) + + case err != nil: + return lnwire.OpaqueReason{}, err + } - return s.EncryptError(false, reason) + return encrypted, nil } // Type returns the identifier for a sphinx error encrypter. @@ -153,7 +256,25 @@ func (s *SphinxErrorEncrypter) Type() EncrypterType { func (s *SphinxErrorEncrypter) Encode(w io.Writer) error { ephemeral := s.EphemeralKey.SerializeCompressed() _, err := w.Write(ephemeral) - return err + if err != nil { + return err + } + + // Stop here for legacy errors. + if !s.attrError { + return nil + } + + var creationTime = uint64(s.CreatedAt.UnixNano()) + + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord(creationTimeType, &creationTime), + ) + if err != nil { + return err + } + + return tlvStream.Encode(w) } // Decode reconstructs the error encrypter's ephemeral public key from the @@ -170,6 +291,30 @@ func (s *SphinxErrorEncrypter) Decode(r io.Reader) error { return err } + // Try decode attributable error structure. + var creationTime uint64 + + tlvStream, err := tlv.NewStream( + tlv.MakePrimitiveRecord(creationTimeType, &creationTime), + ) + if err != nil { + return err + } + + typeMap, err := tlvStream.DecodeWithParsedTypes(r) + if err != nil { + return err + } + + // Return early if this encrypter is not for attributable errors. + if len(typeMap) == 0 { + return nil + } + + // Set attributable error flag and creation time. + s.attrError = true + s.CreatedAt = time.Unix(0, int64(creationTime)) + return nil } @@ -177,9 +322,9 @@ func (s *SphinxErrorEncrypter) Decode(r io.Reader) error { // This intended to be used shortly after Decode, to fully initialize a // SphinxErrorEncrypter. func (s *SphinxErrorEncrypter) Reextract( - extract ErrorEncrypterExtracter) error { + extract SharedSecretGenerator) error { - obfuscator, failcode := extract(s.EphemeralKey) + sharedSecret, failcode := extract(s.EphemeralKey) if failcode != lnwire.CodeNone { // This should never happen, since we already validated that // this obfuscator can be extracted when it was received in the @@ -188,16 +333,9 @@ func (s *SphinxErrorEncrypter) Reextract( "obfuscator, got failcode: %d", failcode) } - sphinxEncrypter, ok := obfuscator.(*SphinxErrorEncrypter) - if !ok { - return fmt.Errorf("incorrect onion error extracter") - } - - // Copy the freshly extracted encrypter. - s.OnionErrorEncrypter = sphinxEncrypter.OnionErrorEncrypter + s.initialize(sharedSecret) return nil - } // A compile time check to ensure SphinxErrorEncrypter implements the diff --git a/htlcswitch/hop/iterator.go b/htlcswitch/hop/iterator.go index c1073b1da9..2a2f4f71d0 100644 --- a/htlcswitch/hop/iterator.go +++ b/htlcswitch/hop/iterator.go @@ -29,10 +29,11 @@ type Iterator interface { // into the passed io.Writer. EncodeNextHop(w io.Writer) error - // ExtractErrorEncrypter returns the ErrorEncrypter needed for this hop, - // along with a failure code to signal if the decoding was successful. - ExtractErrorEncrypter(ErrorEncrypterExtracter) (ErrorEncrypter, - lnwire.FailCode) + // ExtractEncrypterParams extracts the ephemeral key and shared secret + // from the onion packet and returns them to the caller along with a + // failure code to signal if the decoding was successful. + ExtractEncrypterParams(SharedSecretGenerator) ( + *btcec.PublicKey, sphinx.Hash256, lnwire.FailCode) } // sphinxHopIterator is the Sphinx implementation of hop iterator which uses @@ -100,16 +101,21 @@ func (r *sphinxHopIterator) HopPayload() (*Payload, error) { } } -// ExtractErrorEncrypter decodes and returns the ErrorEncrypter for this hop, -// along with a failure code to signal if the decoding was successful. The -// ErrorEncrypter is used to encrypt errors back to the sender in the event that -// a payment fails. +// ExtractEncrypterParams extracts the ephemeral key and shared secret from the +// onion packet and returns them to the caller along with a failure code to +// signal if the decoding was successful. // // NOTE: Part of the HopIterator interface. -func (r *sphinxHopIterator) ExtractErrorEncrypter( - extracter ErrorEncrypterExtracter) (ErrorEncrypter, lnwire.FailCode) { +func (r *sphinxHopIterator) ExtractEncrypterParams( + extracter SharedSecretGenerator) (*btcec.PublicKey, sphinx.Hash256, + lnwire.FailCode) { - return extracter(r.ogPacket.EphemeralKey) + sharedSecret, failCode := extracter(r.ogPacket.EphemeralKey) + if failCode != lnwire.CodeNone { + return nil, sphinx.Hash256{}, failCode + } + + return r.ogPacket.EphemeralKey, sharedSecret, lnwire.CodeNone } // OnionProcessor is responsible for keeping all sphinx dependent parts inside @@ -375,33 +381,26 @@ func (p *OnionProcessor) DecodeHopIterators(id []byte, return resps, nil } -// ExtractErrorEncrypter takes an io.Reader which should contain the onion -// packet as original received by a forwarding node and creates an -// ErrorEncrypter instance using the derived shared secret. In the case that en -// error occurs, a lnwire failure code detailing the parsing failure will be -// returned. -func (p *OnionProcessor) ExtractErrorEncrypter(ephemeralKey *btcec.PublicKey) ( - ErrorEncrypter, lnwire.FailCode) { +// ExtractSharedSecret takes an ephemeral session key as original received by a +// forwarding node and generates the shared secret. In the case that en error +// occurs, a lnwire failure code detailing the parsing failure will be returned. +func (p *OnionProcessor) ExtractSharedSecret(ephemeralKey *btcec.PublicKey) ( + sphinx.Hash256, lnwire.FailCode) { - onionObfuscator, err := sphinx.NewOnionErrorEncrypter( - p.router, ephemeralKey, - ) + sharedSecret, err := p.router.GenerateSharedSecret(ephemeralKey) if err != nil { switch err { case sphinx.ErrInvalidOnionVersion: - return nil, lnwire.CodeInvalidOnionVersion + return sphinx.Hash256{}, lnwire.CodeInvalidOnionVersion case sphinx.ErrInvalidOnionHMAC: - return nil, lnwire.CodeInvalidOnionHmac + return sphinx.Hash256{}, lnwire.CodeInvalidOnionHmac case sphinx.ErrInvalidOnionKey: - return nil, lnwire.CodeInvalidOnionKey + return sphinx.Hash256{}, lnwire.CodeInvalidOnionKey default: log.Errorf("unable to process onion packet: %v", err) - return nil, lnwire.CodeInvalidOnionKey + return sphinx.Hash256{}, lnwire.CodeInvalidOnionKey } } - return &SphinxErrorEncrypter{ - OnionErrorEncrypter: onionObfuscator, - EphemeralKey: ephemeralKey, - }, lnwire.CodeNone + return sharedSecret, lnwire.CodeNone } diff --git a/htlcswitch/hop/payload.go b/htlcswitch/hop/payload.go index d4672766c7..819644cd33 100644 --- a/htlcswitch/hop/payload.go +++ b/htlcswitch/hop/payload.go @@ -97,6 +97,10 @@ type Payload struct { // metadata is additional data that is sent along with the payment to // the payee. metadata []byte + + // AttributableError signals that the sender wants to receive + // attributable errors. + AttributableError bool } // NewLegacyPayload builds a Payload from the amount, cltv, and next hop @@ -119,12 +123,13 @@ func NewLegacyPayload(f *sphinx.HopData) *Payload { // should correspond to the bytes encapsulated in a TLV onion payload. func NewPayloadFromReader(r io.Reader) (*Payload, error) { var ( - cid uint64 - amt uint64 - cltv uint32 - mpp = &record.MPP{} - amp = &record.AMP{} - metadata []byte + cid uint64 + amt uint64 + cltv uint32 + mpp = &record.MPP{} + amp = &record.AMP{} + metadata []byte + attributableError = &record.AttributableError{} ) tlvStream, err := tlv.NewStream( @@ -134,6 +139,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) { mpp.Record(), amp.Record(), record.NewMetadataRecord(&metadata), + attributableError.Record(), ) if err != nil { return nil, err @@ -182,6 +188,12 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) { metadata = nil } + // If no attributable error field was parsed, set the attributable error + // field on the resulting payload to nil. + if _, ok := parsedTypes[record.AttributableErrorOnionType]; !ok { + attributableError = nil + } + // Filter out the custom records. customRecords := NewCustomRecords(parsedTypes) @@ -192,10 +204,11 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) { AmountToForward: lnwire.MilliSatoshi(amt), OutgoingCTLV: cltv, }, - MPP: mpp, - AMP: amp, - metadata: metadata, - customRecords: customRecords, + MPP: mpp, + AMP: amp, + metadata: metadata, + customRecords: customRecords, + AttributableError: attributableError != nil, }, nil } diff --git a/htlcswitch/interceptable_switch.go b/htlcswitch/interceptable_switch.go index 09646bf2d0..df5381e1ad 100644 --- a/htlcswitch/interceptable_switch.go +++ b/htlcswitch/interceptable_switch.go @@ -618,7 +618,10 @@ func (f *interceptedForward) Resume() error { // Fail notifies the intention to Fail an existing hold forward with an // encrypted failure reason. func (f *interceptedForward) Fail(reason []byte) error { - obfuscatedReason := f.packet.obfuscator.IntermediateEncrypt(reason) + obfuscatedReason, err := f.packet.obfuscator.IntermediateEncrypt(reason) + if err != nil { + return err + } return f.resolve(&lnwire.UpdateFailHTLC{ Reason: obfuscatedReason, diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 4fbe905544..7f3dd45f46 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -10,11 +10,13 @@ import ( "sync/atomic" "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btclog" "github.com/davecgh/go-spew/spew" "github.com/go-errors/errors" + sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" @@ -153,9 +155,14 @@ type ChannelLinkConfig struct { DecodeHopIterators func([]byte, []hop.DecodeHopIteratorRequest) ( []hop.DecodeHopIteratorResponse, error) - // ExtractErrorEncrypter function is responsible for decoding HTLC - // Sphinx onion blob, and creating onion failure obfuscator. - ExtractErrorEncrypter hop.ErrorEncrypterExtracter + // ExtractSharedSecret function is responsible for decoding HTLC + // Sphinx onion blob, and deriving the shared secret. + ExtractSharedSecret hop.SharedSecretGenerator + + // CreateErrorEncrypter instantiates an error encrypter based on the + // provided encryption parameters. + CreateErrorEncrypter func(*btcec.PublicKey, + sphinx.Hash256, bool) hop.ErrorEncrypter // FetchLastChannelUpdate retrieves the latest routing policy for a // target channel. This channel will typically be the outgoing channel @@ -3006,9 +3013,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, // Retrieve onion obfuscator from onion blob in order to // produce initial obfuscation of the onion failureCode. - obfuscator, failureCode := chanIterator.ExtractErrorEncrypter( - l.cfg.ExtractErrorEncrypter, - ) + ephemeralKey, sharedSecret, failureCode := chanIterator. + ExtractEncrypterParams(l.cfg.ExtractSharedSecret) if failureCode != lnwire.CodeNone { // If we're unable to process the onion blob than we // should send the malformed htlc error to payment @@ -3022,6 +3028,14 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, continue } + // Instantiate an error encrypter based on the extracted + // encryption parameters. Don't assume attributable errors, + // because first the resolution format needs to be decoded from + // the onion payload. + obfuscator := l.cfg.CreateErrorEncrypter( + ephemeralKey, sharedSecret, false, + ) + heightNow := l.cfg.BestHeight() pld, err := chanIterator.HopPayload() @@ -3051,6 +3065,14 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, continue } + // Now that we've successfully decoded the tlv, we can upgrade + // to the failure message version that the sender supports. + if pld.AttributableError { + obfuscator = l.cfg.CreateErrorEncrypter( + ephemeralKey, sharedSecret, true, + ) + } + fwdInfo := pld.ForwardingInfo() switch fwdInfo.NextHop { diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index 6817ff930c..b178c02ace 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -1576,9 +1576,10 @@ func TestChannelLinkMultiHopDecodeError(t *testing.T) { t.Cleanup(n.stop) // Replace decode function with another which throws an error. - n.carolChannelLink.cfg.ExtractErrorEncrypter = func( - *btcec.PublicKey) (hop.ErrorEncrypter, lnwire.FailCode) { - return nil, lnwire.CodeInvalidOnionVersion + n.carolChannelLink.cfg.ExtractSharedSecret = func( + *btcec.PublicKey) (sphinx.Hash256, lnwire.FailCode) { + + return sphinx.Hash256{}, lnwire.CodeInvalidOnionVersion } carolBandwidthBefore := n.carolChannelLink.Bandwidth() @@ -1962,9 +1963,16 @@ func newSingleLinkTestHarness(t *testing.T, chanAmt, chanReserve btcutil.Amount) return aliceSwitch.ForwardPackets(linkQuit, packets...) }, DecodeHopIterators: decoder.DecodeHopIterators, - ExtractErrorEncrypter: func(*btcec.PublicKey) ( - hop.ErrorEncrypter, lnwire.FailCode) { - return obfuscator, lnwire.CodeNone + ExtractSharedSecret: func(*btcec.PublicKey) ( + sphinx.Hash256, lnwire.FailCode) { + + return sphinx.Hash256{}, lnwire.CodeNone + }, + CreateErrorEncrypter: func(*btcec.PublicKey, + sphinx.Hash256, + bool) hop.ErrorEncrypter { + + return obfuscator }, FetchLastChannelUpdate: mockGetChanUpdateMessage, PreimageCache: pCache, @@ -4413,10 +4421,15 @@ func (h *persistentLinkHarness) restartLink( return aliceSwitch.ForwardPackets(linkQuit, packets...) }, DecodeHopIterators: decoder.DecodeHopIterators, - ExtractErrorEncrypter: func(*btcec.PublicKey) ( - hop.ErrorEncrypter, lnwire.FailCode) { + ExtractSharedSecret: func(*btcec.PublicKey) ( + sphinx.Hash256, lnwire.FailCode) { + + return sphinx.Hash256{}, lnwire.CodeNone + }, + CreateErrorEncrypter: func(*btcec.PublicKey, + sphinx.Hash256, bool) hop.ErrorEncrypter { - return obfuscator, lnwire.CodeNone + return obfuscator }, FetchLastChannelUpdate: mockGetChanUpdateMessage, PreimageCache: pCache, diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index bb36eda20b..9313328ac0 100644 --- a/htlcswitch/mock.go +++ b/htlcswitch/mock.go @@ -339,11 +339,16 @@ func (r *mockHopIterator) ExtraOnionBlob() []byte { return nil } -func (r *mockHopIterator) ExtractErrorEncrypter( - extracter hop.ErrorEncrypterExtracter) (hop.ErrorEncrypter, +func (r *mockHopIterator) ExtractEncrypterParams( + extracter hop.SharedSecretGenerator) (*btcec.PublicKey, sphinx.Hash256, lnwire.FailCode) { - return extracter(nil) + sharedSecret, failCode := extracter(nil) + if failCode != lnwire.CodeNone { + return nil, sphinx.Hash256{}, failCode + } + + return &btcec.PublicKey{}, sharedSecret, lnwire.CodeNone } func (r *mockHopIterator) EncodeNextHop(w io.Writer) error { @@ -415,7 +420,7 @@ func (o *mockObfuscator) Decode(r io.Reader) error { } func (o *mockObfuscator) Reextract( - extracter hop.ErrorEncrypterExtracter) error { + extracter hop.SharedSecretGenerator) error { return nil } @@ -436,17 +441,21 @@ func (o *mockObfuscator) EncryptFirstHop(failure lnwire.FailureMessage) ( return b.Bytes(), nil } -func (o *mockObfuscator) IntermediateEncrypt(reason lnwire.OpaqueReason) lnwire.OpaqueReason { - return reason +func (o *mockObfuscator) IntermediateEncrypt(reason lnwire.OpaqueReason) ( + lnwire.OpaqueReason, error) { + + return reason, nil } -func (o *mockObfuscator) EncryptMalformedError(reason lnwire.OpaqueReason) lnwire.OpaqueReason { +func (o *mockObfuscator) EncryptMalformedError(reason lnwire.OpaqueReason) ( + lnwire.OpaqueReason, error) { + var b bytes.Buffer b.Write(fakeHmac) b.Write(reason) - return b.Bytes() + return b.Bytes(), nil } // mockDeobfuscator mock implementation of the failure deobfuscator which diff --git a/htlcswitch/switch.go b/htlcswitch/switch.go index 6f632be354..16e66fe372 100644 --- a/htlcswitch/switch.go +++ b/htlcswitch/switch.go @@ -160,10 +160,10 @@ type Config struct { // forwarding packages, and ack settles and fails contained within them. SwitchPackager channeldb.FwdOperator - // ExtractErrorEncrypter is an interface allowing switch to reextract + // ExtractSharedSecret is an interface allowing switch to reextract // error encrypters stored in the circuit map on restarts, since they // are not stored directly within the database. - ExtractErrorEncrypter hop.ErrorEncrypterExtracter + ExtractSharedSecret hop.SharedSecretGenerator // FetchLastChannelUpdate retrieves the latest routing policy for a // target channel. This channel will typically be the outgoing channel @@ -354,11 +354,11 @@ func New(cfg Config, currentHeight uint32) (*Switch, error) { resStore := newResolutionStore(cfg.DB) circuitMap, err := NewCircuitMap(&CircuitMapConfig{ - DB: cfg.DB, - FetchAllOpenChannels: cfg.FetchAllOpenChannels, - FetchClosedChannels: cfg.FetchClosedChannels, - ExtractErrorEncrypter: cfg.ExtractErrorEncrypter, - CheckResolutionMsg: resStore.checkResolutionMsg, + DB: cfg.DB, + FetchAllOpenChannels: cfg.FetchAllOpenChannels, + FetchClosedChannels: cfg.FetchClosedChannels, + ExtractSharedSecret: cfg.ExtractSharedSecret, + CheckResolutionMsg: resStore.checkResolutionMsg, }) if err != nil { return nil, err @@ -1322,16 +1322,24 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error { packet.incomingChanID, packet.incomingHTLCID, packet.outgoingChanID, packet.outgoingHTLCID) - fail.Reason = circuit.ErrorEncrypter.EncryptMalformedError( - fail.Reason, - ) + fail.Reason, err = circuit.ErrorEncrypter. + EncryptMalformedError(fail.Reason) + if err != nil { + err = fmt.Errorf("unable to obfuscate "+ + "malformed error: %v", err) + log.Error(err) + } default: // Otherwise, it's a forwarded error, so we'll perform a // wrapper encryption as normal. - fail.Reason = circuit.ErrorEncrypter.IntermediateEncrypt( - fail.Reason, - ) + fail.Reason, err = circuit.ErrorEncrypter. + IntermediateEncrypt(fail.Reason) + if err != nil { + err = fmt.Errorf("unable to obfuscate "+ + "intermediate error: %v", err) + log.Error(err) + } } } else if !isFail && circuit.Outgoing != nil { // If this is an HTLC settle, and it wasn't from a diff --git a/htlcswitch/switch_test.go b/htlcswitch/switch_test.go index 28f3d9e2c8..b2317365cc 100644 --- a/htlcswitch/switch_test.go +++ b/htlcswitch/switch_test.go @@ -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, }, } @@ -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}, }, diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index c1bfb1f553..fb4315c3ee 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -1131,9 +1131,15 @@ func (h *hopNetwork) createChannelLink(server, peer *mockServer, return server.htlcSwitch.ForwardPackets(linkQuit, packets...) }, DecodeHopIterators: decoder.DecodeHopIterators, - ExtractErrorEncrypter: func(*btcec.PublicKey) ( - hop.ErrorEncrypter, lnwire.FailCode) { - return h.obfuscator, lnwire.CodeNone + ExtractSharedSecret: func(*btcec.PublicKey) ( + sphinx.Hash256, lnwire.FailCode) { + + return sphinx.Hash256{}, lnwire.CodeNone + }, + CreateErrorEncrypter: func(*btcec.PublicKey, + sphinx.Hash256, bool) hop.ErrorEncrypter { + + return h.obfuscator }, FetchLastChannelUpdate: mockGetChanUpdateMessage, Registry: server.registry, diff --git a/itest/lnd_multi-hop-error-propagation_test.go b/itest/lnd_multi-hop-error-propagation_test.go index 3941accb0b..ac2e1955c3 100644 --- a/itest/lnd_multi-hop-error-propagation_test.go +++ b/itest/lnd_multi-hop-error-propagation_test.go @@ -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) diff --git a/lnrpc/routerrpc/config.go b/lnrpc/routerrpc/config.go index adcc84e800..b2340126a6 100644 --- a/lnrpc/routerrpc/config.go +++ b/lnrpc/routerrpc/config.go @@ -87,5 +87,6 @@ func GetRoutingConfig(cfg *Config) *RoutingConfig { NodeWeight: cfg.BimodalConfig.NodeWeight, DecayTime: cfg.BimodalConfig.DecayTime, }, + AttrErrors: cfg.AttrErrors, } } diff --git a/lnrpc/routerrpc/routing_config.go b/lnrpc/routerrpc/routing_config.go index 31494d5464..1e0084a3ca 100644 --- a/lnrpc/routerrpc/routing_config.go +++ b/lnrpc/routerrpc/routing_config.go @@ -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. diff --git a/lnwire/features.go b/lnwire/features.go index dd0abc3920..15314dbb7c 100644 --- a/lnwire/features.go +++ b/lnwire/features.go @@ -151,6 +151,14 @@ const ( // addresses for cooperative closure addresses. ShutdownAnySegwitOptional FeatureBit = 27 + // AttributableErrorsRequired is a required feature bit that signals + // that the node is able to generate and relay attributable errors. + AttributableErrorsRequired FeatureBit = 36 + + // AttributableErrorsOptional is an optional feature bit that signals + // that the node is able to generate and relay attributable errors. + AttributableErrorsOptional FeatureBit = 37 + // AMPRequired is a required feature bit that signals that the receiver // of a payment supports accepts spontaneous payments, i.e. // sender-generated preimages according to BOLT XX. @@ -291,6 +299,8 @@ var Features = map[FeatureBit]string{ ZeroConfOptional: "zero-conf", ShutdownAnySegwitRequired: "shutdown-any-segwit", ShutdownAnySegwitOptional: "shutdown-any-segwit", + AttributableErrorsRequired: "attributable-errors", + AttributableErrorsOptional: "attributable-errors", } // RawFeatureVector represents a set of feature bits as defined in BOLT-09. A diff --git a/peer/brontide.go b/peer/brontide.go index 591f6601a9..b350074ad6 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -17,6 +17,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btclog" "github.com/davecgh/go-spew/spew" + sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/buffer" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/chainntnfs" @@ -966,9 +967,17 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint, //nolint:lll linkCfg := htlcswitch.ChannelLinkConfig{ - Peer: p, - DecodeHopIterators: p.cfg.Sphinx.DecodeHopIterators, - ExtractErrorEncrypter: p.cfg.Sphinx.ExtractErrorEncrypter, + Peer: p, + DecodeHopIterators: p.cfg.Sphinx.DecodeHopIterators, + ExtractSharedSecret: p.cfg.Sphinx.ExtractSharedSecret, + CreateErrorEncrypter: func(ephemeralKey *btcec.PublicKey, + sharedSecret sphinx.Hash256, + attrError bool) hop.ErrorEncrypter { + + return hop.NewSphinxErrorEncrypter( + ephemeralKey, sharedSecret, attrError, + ) + }, FetchLastChannelUpdate: p.cfg.FetchLastChanUpdate, HodlMask: p.cfg.Hodl.Mask(), Registry: p.cfg.Invoices, diff --git a/record/attr_error.go b/record/attr_error.go new file mode 100644 index 0000000000..8b50d72d8b --- /dev/null +++ b/record/attr_error.go @@ -0,0 +1,82 @@ +package record + +import ( + "io" + + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // AttributableErrorOnionType is the type used in the onion for the fat + // error message structure. + AttributableErrorOnionType tlv.Type = 20 + + // attributableErrorLength is the byte size of the attributable error + // record. + attributableErrorLength = 0 +) + +// AttributableError is a record that encodes the fields necessary for fat +// errors. +type AttributableError struct{} + +// NewAttributableError generates a new AttributableError record with the given +// structure. +func NewAttributableError() *AttributableError { + return &AttributableError{} +} + +// AttributableErrorEncoder writes the AttributableError record to the provided +// io.Writer. +func AttributableErrorEncoder(_ io.Writer, val interface{}, + _ *[8]byte) error { + + _, ok := val.(*AttributableError) + if !ok { + return tlv.NewTypeForEncodingErr(val, "AttributableError") + } + + return nil +} + +// AttributableErrorDecoder reads the AttributableError record to the provided +// io.Reader. +func AttributableErrorDecoder(_ io.Reader, val interface{}, _ *[8]byte, + l uint64) error { + + _, ok := val.(*AttributableError) + if !ok || l != attributableErrorLength { + return tlv.NewTypeForDecodingErr( + val, "AttributableError", l, attributableErrorLength, + ) + } + + return nil +} + +// Record returns a tlv.Record that can be used to encode or decode this record. +func (r *AttributableError) Record() tlv.Record { + size := func() uint64 { + return attributableErrorLength + } + + return tlv.MakeDynamicRecord( + AttributableErrorOnionType, r, size, AttributableErrorEncoder, + AttributableErrorDecoder, + ) +} + +// PayloadSize returns the size this record takes up in encoded form. +func (r *AttributableError) PayloadSize() uint64 { + return attributableErrorLength +} + +// String returns a human-readable representation of the AttributableError +// payload field. +func (r *AttributableError) String() string { + if r == nil { + return "" + } + + return "attr errors" +} diff --git a/routing/mock_graph_test.go b/routing/mock_graph_test.go index dfbfd6abae..6063914ebc 100644 --- a/routing/mock_graph_test.go +++ b/routing/mock_graph_test.go @@ -51,7 +51,7 @@ func newMockNode(id byte) *mockNode { // nil, this node is considered to be the sender of the payment. The route // parameter describes the remaining route from this node onwards. If route.next // is nil, this node is the final hop. -func (m *mockNode) fwd(from *mockNode, route *hop) (htlcResult, error) { +func (m *mockNode) fwd(from *mockNode, route *mockHop) (htlcResult, error) { next := route.next // Get the incoming channel, if any. @@ -258,24 +258,24 @@ type htlcResult struct { failure lnwire.FailureMessage } -// hop describes one hop of a route. -type hop struct { +// mockHop describes one mockHop of a route. +type mockHop struct { node *mockNode amtToFwd lnwire.MilliSatoshi - next *hop + next *mockHop } // sendHtlc sends out an htlc on the mock network and synchronously returns the // final resolution of the htlc. func (m *mockGraph) sendHtlc(route *route.Route) (htlcResult, error) { - var next *hop + var next *mockHop // Convert the route into a structure that is suitable for recursive // processing. for i := len(route.Hops) - 1; i >= 0; i-- { routeHop := route.Hops[i] node := m.nodes[routeHop.PubKeyBytes] - next = &hop{ + next = &mockHop{ node: node, next: next, amtToFwd: routeHop.AmtToForward, @@ -284,7 +284,7 @@ func (m *mockGraph) sendHtlc(route *route.Route) (htlcResult, error) { // Create the starting hop instance. source := m.nodes[route.SourcePubKey] - next = &hop{ + next = &mockHop{ node: source, next: next, amtToFwd: route.TotalAmount, diff --git a/routing/pathfind.go b/routing/pathfind.go index e293d320de..cf5845fb2f 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -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" @@ -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 @@ -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 @@ -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 @@ -250,6 +284,7 @@ func newRoute(sourceVertex route.Vertex, CustomRecords: customRecords, MPP: mpp, Metadata: metadata, + AttrError: attributableErrors, } hops = append([]*route.Hop{currentHop}, hops...) @@ -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 diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index 4224a4f638..ed69f9b7d5 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -959,6 +959,7 @@ func runFindLowestFeePath(t *testing.T, useCache bool) { cltvDelta: finalHopCLTV, records: nil, }, + false, ) require.NoError(t, err, "unable to create path") @@ -1101,6 +1102,7 @@ func testBasicGraphPathFindingCase(t *testing.T, graphInstance *testGraphInstanc cltvDelta: finalHopCLTV, records: nil, }, + false, ) require.NoError(t, err, "unable to create path") @@ -1639,6 +1641,7 @@ func TestNewRoute(t *testing.T) { paymentAddr: testCase.paymentAddr, metadata: testCase.metadata, }, + false, ) if testCase.expectError { @@ -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") @@ -2964,6 +2968,7 @@ func runNoCycle(t *testing.T, useCache bool) { cltvDelta: finalHopCLTV, records: nil, }, + false, ) require.NoError(t, err, "unable to create path") diff --git a/routing/payment_lifecycle.go b/routing/payment_lifecycle.go index c6bd7e08e8..e0b25f3b2a 100644 --- a/routing/payment_lifecycle.go +++ b/routing/payment_lifecycle.go @@ -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" @@ -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. diff --git a/routing/payment_session.go b/routing/payment_session.go index 8b181a04d9..9228f4b9a8 100644 --- a/routing/payment_session.go +++ b/routing/payment_session.go @@ -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 diff --git a/routing/route/route.go b/routing/route/route.go index 5992bd4046..f76b2e6c23 100644 --- a/routing/route/route.go +++ b/routing/route/route.go @@ -131,6 +131,9 @@ type Hop struct { // Metadata is additional data that is sent along with the payment to // the payee. Metadata []byte + + // AttrError signals that the sender wants to use attributable errors. + AttrError bool } // Copy returns a deep copy of the Hop. @@ -176,6 +179,12 @@ func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error { record.NewLockTimeRecord(&h.OutgoingTimeLock), ) + // Add attributable error structure if used. + if h.AttrError { + attrErrorRecord := record.NewAttributableError() + records = append(records, attrErrorRecord.Record()) + } + // BOLT 04 says the next_hop_id should be omitted for the final hop, // but present for all others. // @@ -255,6 +264,11 @@ func (h *Hop) PayloadSize(nextChanID uint64) uint64 { tlv.SizeTUint64(uint64(h.OutgoingTimeLock)), ) + // Add attributable error structure. + if h.AttrError { + addRecord(record.AttributableErrorOnionType, 0) + } + // Add next hop if present. if nextChanID != 0 { addRecord(record.NextHopOnionType, 8) diff --git a/routing/router.go b/routing/router.go index a38980bbaf..2c8164eb27 100644 --- a/routing/router.go +++ b/routing/router.go @@ -1805,6 +1805,7 @@ func (r *ChannelRouter) FindRoute(source, target route.Vertex, cltvDelta: finalExpiry, records: destCustomRecords, }, + r.cfg.PathFindingConfig.AttrErrors, ) if err != nil { return nil, 0, err @@ -2798,6 +2799,7 @@ func (r *ChannelRouter) BuildRoute(amt *lnwire.MilliSatoshi, records: nil, paymentAddr: payAddr, }, + r.cfg.PathFindingConfig.AttrErrors, ) } diff --git a/sample-lnd.conf b/sample-lnd.conf index bf738cadbe..7d050367f7 100644 --- a/sample-lnd.conf +++ b/sample-lnd.conf @@ -1393,6 +1393,9 @@ ; failures in channels. ; routerrpc.bimodal.decaytime=168h +; Indicates whether attributable errors should be requested if the whole route +; supports it. (default=false) +; routerrpc.attrerrors=false [workers] diff --git a/server.go b/server.go index 5b421ae2f5..641a95a030 100644 --- a/server.go +++ b/server.go @@ -654,7 +654,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, }, FwdingLog: dbs.ChanStateDB.ForwardingLog(), SwitchPackager: channeldb.NewSwitchPackager(), - ExtractErrorEncrypter: s.sphinx.ExtractErrorEncrypter, + ExtractSharedSecret: s.sphinx.ExtractSharedSecret, FetchLastChannelUpdate: s.fetchLastChanUpdate(), Notifier: s.cc.ChainNotifier, HtlcNotifier: s.htlcNotifier, @@ -940,6 +940,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, ), AttemptCostPPM: routingConfig.AttemptCostPPM, MinProbability: routingConfig.MinRouteProbability, + AttrErrors: routingConfig.AttrErrors, } sourceNode, err := chanGraph.SourceNode()