From b22b1be35a77a191eb4ed90ba1bed24e6b880153 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Mon, 16 Jan 2023 09:25:13 +0100 Subject: [PATCH 1/6] htlcswitch: return error from all encrypt methods --- htlcswitch/hop/error_encryptor.go | 12 ++++++------ htlcswitch/interceptable_switch.go | 5 ++++- htlcswitch/mock.go | 12 ++++++++---- htlcswitch/switch.go | 20 ++++++++++++++------ 4 files changed, 32 insertions(+), 17 deletions(-) diff --git a/htlcswitch/hop/error_encryptor.go b/htlcswitch/hop/error_encryptor.go index 7b6a3dd1a5..de8efbb85d 100644 --- a/htlcswitch/hop/error_encryptor.go +++ b/htlcswitch/hop/error_encryptor.go @@ -47,12 +47,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. @@ -125,9 +125,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.EncryptError(true, reason), nil } // IntermediateEncrypt wraps an already encrypted opaque reason error in an @@ -138,9 +138,9 @@ 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) { - return s.EncryptError(false, reason) + return s.EncryptError(false, reason), nil } // Type returns the identifier for a sphinx error encrypter. 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/mock.go b/htlcswitch/mock.go index bb36eda20b..d816dd17a6 100644 --- a/htlcswitch/mock.go +++ b/htlcswitch/mock.go @@ -436,17 +436,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..0319ff4cbb 100644 --- a/htlcswitch/switch.go +++ b/htlcswitch/switch.go @@ -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 From e74a13a730484c64bcfba2ed53c99f01c7515f43 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 17 Jan 2023 13:56:26 +0100 Subject: [PATCH 2/6] htlcswitch: bump lightning-onion --- go.mod | 2 ++ go.sum | 4 ++-- htlcswitch/hop/iterator.go | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) 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/hop/iterator.go b/htlcswitch/hop/iterator.go index c1073b1da9..a00a23e50a 100644 --- a/htlcswitch/hop/iterator.go +++ b/htlcswitch/hop/iterator.go @@ -383,9 +383,7 @@ func (p *OnionProcessor) DecodeHopIterators(id []byte, func (p *OnionProcessor) ExtractErrorEncrypter(ephemeralKey *btcec.PublicKey) ( ErrorEncrypter, lnwire.FailCode) { - onionObfuscator, err := sphinx.NewOnionErrorEncrypter( - p.router, ephemeralKey, - ) + sharedSecret, err := p.router.GenerateSharedSecret(ephemeralKey) if err != nil { switch err { case sphinx.ErrInvalidOnionVersion: @@ -400,6 +398,8 @@ func (p *OnionProcessor) ExtractErrorEncrypter(ephemeralKey *btcec.PublicKey) ( } } + onionObfuscator := sphinx.NewOnionErrorEncrypter(sharedSecret) + return &SphinxErrorEncrypter{ OnionErrorEncrypter: onionObfuscator, EphemeralKey: ephemeralKey, From ef15be1a9fda87cb2a0f60f8b0ba73b27fa86f69 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Mon, 23 Jan 2023 11:57:20 +0100 Subject: [PATCH 3/6] htlcswitch: split shared secret extraction and encrypter instantiation Preparation for the instantiation of an attributable error encrypter. This gets rid of the sphinx encrypter instantiation in OnionProcessor. This would otherwise be problematic when an attributable encrypter would need to be created there without having access to the error structure. --- htlcswitch/circuit.go | 2 +- htlcswitch/circuit_map.go | 6 ++-- htlcswitch/circuit_test.go | 32 ++++++++--------- htlcswitch/hop/error_encryptor.go | 58 ++++++++++++++++++++----------- htlcswitch/hop/iterator.go | 55 ++++++++++++++--------------- htlcswitch/link.go | 24 +++++++++---- htlcswitch/link_test.go | 30 +++++++++++----- htlcswitch/mock.go | 13 ++++--- htlcswitch/switch.go | 14 ++++---- htlcswitch/test_utils.go | 12 +++++-- peer/brontide.go | 14 ++++++-- server.go | 2 +- 12 files changed, 160 insertions(+), 102 deletions(-) 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..a87bb03e45 100644 --- a/htlcswitch/circuit_test.go +++ b/htlcswitch/circuit_test.go @@ -66,16 +66,16 @@ 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, + ) // We also set this error extracter on startup, otherwise it will be nil // at compile-time. @@ -107,10 +107,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 { @@ -217,7 +217,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 +646,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/hop/error_encryptor.go b/htlcswitch/hop/error_encryptor.go index de8efbb85d..815210d9fd 100644 --- a/htlcswitch/hop/error_encryptor.go +++ b/htlcswitch/hop/error_encryptor.go @@ -27,9 +27,9 @@ const ( EncrypterTypeMock = 2 ) -// ErrorEncrypterExtracter defines a function signature that extracts an -// ErrorEncrypter from an sphinx OnionPacket. -type ErrorEncrypterExtracter func(*btcec.PublicKey) (ErrorEncrypter, +// 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 @@ -66,12 +66,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 @@ -84,20 +85,42 @@ type SphinxErrorEncrypter struct { EphemeralKey *btcec.PublicKey } -// 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{}, } } +// 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) *SphinxErrorEncrypter { + + encrypter := &SphinxErrorEncrypter{ + EphemeralKey: ephemeralKey, + } + + encrypter.initialize(sharedSecret) + + return encrypter +} + +func (s *SphinxErrorEncrypter) initialize(sharedSecret sphinx.Hash256) { + s.OnionErrorEncrypter = sphinx.NewOnionErrorEncrypter( + sharedSecret, + ) +} + // EncryptFirstHop transforms a concrete failure message into an encrypted // opaque failure reason. This method will be used at the source that the error // occurs. It differs from BackwardObfuscate slightly, in that it computes a @@ -177,9 +200,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 +211,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 a00a23e50a..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) { 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 } } - onionObfuscator := sphinx.NewOnionErrorEncrypter(sharedSecret) - - return &SphinxErrorEncrypter{ - OnionErrorEncrypter: onionObfuscator, - EphemeralKey: ephemeralKey, - }, lnwire.CodeNone + return sharedSecret, lnwire.CodeNone } diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 4fbe905544..e504114c32 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) 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,12 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, continue } + // Instantiate an error encrypter based on the extracted + // encryption parameters. + obfuscator := l.cfg.CreateErrorEncrypter( + ephemeralKey, sharedSecret, + ) + heightNow := l.cfg.BestHeight() pld, err := chanIterator.HopPayload() diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index 6817ff930c..7216f01bfe 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,15 @@ 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) hop.ErrorEncrypter { + + return obfuscator }, FetchLastChannelUpdate: mockGetChanUpdateMessage, PreimageCache: pCache, @@ -4413,10 +4420,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) hop.ErrorEncrypter { - return obfuscator, lnwire.CodeNone + return obfuscator }, FetchLastChannelUpdate: mockGetChanUpdateMessage, PreimageCache: pCache, diff --git a/htlcswitch/mock.go b/htlcswitch/mock.go index d816dd17a6..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 } diff --git a/htlcswitch/switch.go b/htlcswitch/switch.go index 0319ff4cbb..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 diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index c1bfb1f553..67330be0b4 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) hop.ErrorEncrypter { + + return h.obfuscator }, FetchLastChannelUpdate: mockGetChanUpdateMessage, Registry: server.registry, diff --git a/peer/brontide.go b/peer/brontide.go index 591f6601a9..f6e916d3bf 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,16 @@ 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) hop.ErrorEncrypter { + + return hop.NewSphinxErrorEncrypter( + ephemeralKey, sharedSecret, + ) + }, FetchLastChannelUpdate: p.cfg.FetchLastChanUpdate, HodlMask: p.cfg.Hodl.Mask(), Registry: p.cfg.Invoices, diff --git a/server.go b/server.go index 5b421ae2f5..28b35b31a7 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, From f942e03dfaf520054914b22da8cdc7dbad605d8d Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Mon, 23 Jan 2023 12:30:02 +0100 Subject: [PATCH 4/6] htlcswitch: support fat error generation and relay --- feature/default_sets.go | 5 + htlcswitch/circuit_test.go | 19 +++- htlcswitch/hop/error_encryptor.go | 148 +++++++++++++++++++++++++++--- htlcswitch/hop/payload.go | 33 +++++-- htlcswitch/link.go | 16 +++- htlcswitch/link_test.go | 5 +- htlcswitch/test_utils.go | 2 +- lnwire/features.go | 10 ++ peer/brontide.go | 5 +- record/attr_error.go | 82 +++++++++++++++++ 10 files changed, 293 insertions(+), 32 deletions(-) create mode 100644 record/attr_error.go 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/htlcswitch/circuit_test.go b/htlcswitch/circuit_test.go index a87bb03e45..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() { @@ -74,12 +78,17 @@ func initTestExtracter() { } testExtracter = hop.NewSphinxErrorEncrypter( - testEphemeralKey, sharedSecret, + 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 @@ -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 diff --git a/htlcswitch/hop/error_encryptor.go b/htlcswitch/hop/error_encryptor.go index 815210d9fd..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,6 +49,8 @@ const ( EncrypterTypeMock = 2 ) +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, @@ -80,9 +104,12 @@ 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 } // NewSphinxErrorEncrypterUninitialized initializes a blank sphinx error @@ -94,8 +121,7 @@ type SphinxErrorEncrypter struct { // OnionProcessor. func NewSphinxErrorEncrypterUninitialized() *SphinxErrorEncrypter { return &SphinxErrorEncrypter{ - OnionErrorEncrypter: nil, - EphemeralKey: &btcec.PublicKey{}, + EphemeralKey: &btcec.PublicKey{}, } } @@ -104,10 +130,18 @@ func NewSphinxErrorEncrypterUninitialized() *SphinxErrorEncrypter { // SphinxErrorEncrypter, use the NewSphinxErrorEncrypterUninitialized // constructor. func NewSphinxErrorEncrypter(ephemeralKey *btcec.PublicKey, - sharedSecret sphinx.Hash256) *SphinxErrorEncrypter { + 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) @@ -116,9 +150,40 @@ func NewSphinxErrorEncrypter(ephemeralKey *btcec.PublicKey, } func (s *SphinxErrorEncrypter) initialize(sharedSecret sphinx.Hash256) { - s.OnionErrorEncrypter = sphinx.NewOnionErrorEncrypter( - sharedSecret, - ) + 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") + } } // EncryptFirstHop transforms a concrete failure message into an encrypted @@ -135,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 @@ -150,7 +213,7 @@ func (s *SphinxErrorEncrypter) EncryptFirstHop( func (s *SphinxErrorEncrypter) EncryptMalformedError( reason lnwire.OpaqueReason) (lnwire.OpaqueReason, error) { - return s.EncryptError(true, reason), nil + return s.encrypt(true, reason) } // IntermediateEncrypt wraps an already encrypted opaque reason error in an @@ -163,7 +226,24 @@ func (s *SphinxErrorEncrypter) EncryptMalformedError( func (s *SphinxErrorEncrypter) IntermediateEncrypt( reason lnwire.OpaqueReason) (lnwire.OpaqueReason, error) { - return s.EncryptError(false, reason), nil + 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 encrypted, nil } // Type returns the identifier for a sphinx error encrypter. @@ -176,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 @@ -193,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 } 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/link.go b/htlcswitch/link.go index e504114c32..7f3dd45f46 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -162,7 +162,7 @@ type ChannelLinkConfig struct { // CreateErrorEncrypter instantiates an error encrypter based on the // provided encryption parameters. CreateErrorEncrypter func(*btcec.PublicKey, - sphinx.Hash256) hop.ErrorEncrypter + sphinx.Hash256, bool) hop.ErrorEncrypter // FetchLastChannelUpdate retrieves the latest routing policy for a // target channel. This channel will typically be the outgoing channel @@ -3029,9 +3029,11 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg, } // Instantiate an error encrypter based on the extracted - // encryption parameters. + // 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, + ephemeralKey, sharedSecret, false, ) heightNow := l.cfg.BestHeight() @@ -3063,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 7216f01bfe..b178c02ace 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -1969,7 +1969,8 @@ func newSingleLinkTestHarness(t *testing.T, chanAmt, chanReserve btcutil.Amount) return sphinx.Hash256{}, lnwire.CodeNone }, CreateErrorEncrypter: func(*btcec.PublicKey, - sphinx.Hash256) hop.ErrorEncrypter { + sphinx.Hash256, + bool) hop.ErrorEncrypter { return obfuscator }, @@ -4426,7 +4427,7 @@ func (h *persistentLinkHarness) restartLink( return sphinx.Hash256{}, lnwire.CodeNone }, CreateErrorEncrypter: func(*btcec.PublicKey, - sphinx.Hash256) hop.ErrorEncrypter { + sphinx.Hash256, bool) hop.ErrorEncrypter { return obfuscator }, diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go index 67330be0b4..fb4315c3ee 100644 --- a/htlcswitch/test_utils.go +++ b/htlcswitch/test_utils.go @@ -1137,7 +1137,7 @@ func (h *hopNetwork) createChannelLink(server, peer *mockServer, return sphinx.Hash256{}, lnwire.CodeNone }, CreateErrorEncrypter: func(*btcec.PublicKey, - sphinx.Hash256) hop.ErrorEncrypter { + sphinx.Hash256, bool) hop.ErrorEncrypter { return h.obfuscator }, 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 f6e916d3bf..b350074ad6 100644 --- a/peer/brontide.go +++ b/peer/brontide.go @@ -971,10 +971,11 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint, DecodeHopIterators: p.cfg.Sphinx.DecodeHopIterators, ExtractSharedSecret: p.cfg.Sphinx.ExtractSharedSecret, CreateErrorEncrypter: func(ephemeralKey *btcec.PublicKey, - sharedSecret sphinx.Hash256) hop.ErrorEncrypter { + sharedSecret sphinx.Hash256, + attrError bool) hop.ErrorEncrypter { return hop.NewSphinxErrorEncrypter( - ephemeralKey, sharedSecret, + ephemeralKey, sharedSecret, attrError, ) }, FetchLastChannelUpdate: p.cfg.FetchLastChanUpdate, 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" +} From cba6e932ef44846fde13c1013fadbb8e76f994f8 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 22 Jun 2023 11:29:34 +0200 Subject: [PATCH 5/6] routing/test: rename hop to mockHop To avoid a collision with the htlcswitch/hop package. --- routing/mock_graph_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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, From c16a9f77a6b71e1b064d76d7d452f5bf20cf7d67 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Wed, 7 Dec 2022 12:23:11 +0100 Subject: [PATCH 6/6] htlcswitch: use fat errors as a sender --- channeldb/payments.go | 13 ++++ htlcswitch/failure.go | 63 +++++++++++++++++-- htlcswitch/failure_test.go | 2 +- htlcswitch/switch_test.go | 4 +- itest/lnd_multi-hop-error-propagation_test.go | 34 +++++++++- lnrpc/routerrpc/config.go | 1 + lnrpc/routerrpc/routing_config.go | 4 ++ routing/pathfind.go | 41 +++++++++++- routing/pathfind_test.go | 5 ++ routing/payment_lifecycle.go | 14 +++-- routing/payment_session.go | 1 + routing/route/route.go | 14 +++++ routing/router.go | 2 + sample-lnd.conf | 3 + server.go | 1 + 15 files changed, 186 insertions(+), 16 deletions(-) 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/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/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/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/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 28b35b31a7..641a95a030 100644 --- a/server.go +++ b/server.go @@ -940,6 +940,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr, ), AttemptCostPPM: routingConfig.AttemptCostPPM, MinProbability: routingConfig.MinRouteProbability, + AttrErrors: routingConfig.AttrErrors, } sourceNode, err := chanGraph.SourceNode()