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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,7 @@ replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-d
// well).
go 1.24.6

// Temporary replace until dependent PR is merged in lightning-onion.
replace github.com/lightningnetwork/lightning-onion => github.com/joostjager/lightning-onion v0.0.0-20250630141312-2898b9c46c4e

retract v0.0.2
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,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-20250630141312-2898b9c46c4e h1:kwxUmYn+qyX4olGy7TxgUeXpmnaMjf4+/bn9Ke9w0GU=
github.com/joostjager/lightning-onion v0.0.0-20250630141312-2898b9c46c4e/go.mod h1:EDqJ3MuZIbMq0QI1czTIKDJ/GS8S14RXPwapHw8cw6w=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/jrick/logrotate v1.1.2 h1:6ePk462NCX7TfKtNp5JJ7MbA2YIslkpfgP03TlTYMN0=
Expand Down Expand Up @@ -368,8 +370,6 @@ github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3
github.com/lightninglabs/neutrino/cache v1.1.2/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo=
github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display h1:Y2WiPkBS/00EiEg0qp0FhehxnQfk3vv8U6Xt3nN+rTY=
github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9 h1:6D3LrdagJweLLdFm1JNodZsBk6iU4TTsBBFLQ4yiXfI=
github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9/go.mod h1:EDqJ3MuZIbMq0QI1czTIKDJ/GS8S14RXPwapHw8cw6w=
github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI=
github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U=
github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0=
Expand Down
7 changes: 4 additions & 3 deletions htlcswitch/circuit.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,17 +199,18 @@ 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.
c.ErrorEncrypter = NewMockObfuscator()

case hop.EncrypterTypeIntroduction:
c.ErrorEncrypter = hop.NewIntroductionErrorEncrypter()
c.ErrorEncrypter =
hop.NewIntroductionErrorEncrypterUninitialized()

case hop.EncrypterTypeRelaying:
c.ErrorEncrypter = hop.NewRelayingErrorEncrypter()
c.ErrorEncrypter = hop.NewRelayingErrorEncrypterUninitialized()

default:
return UnknownEncrypterType(encrypterType)
Expand Down
10 changes: 4 additions & 6 deletions htlcswitch/circuit_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ type CircuitMapConfig struct {
FetchClosedChannels func(
pendingOnly bool) ([]*channeldb.ChannelCloseSummary, error)

// ExtractErrorEncrypter derives the shared secret used to encrypt
// errors from the obfuscator's ephemeral public key.
ExtractErrorEncrypter hop.ErrorEncrypterExtracter
// ExtractSharedSecret derives the shared secret used to encrypt errors
// from the obfuscator's ephemeral public key.
ExtractSharedSecret hop.SharedSecretGenerator

// CheckResolutionMsg checks whether a given resolution message exists
// for the passed CircuitKey.
Expand Down Expand Up @@ -632,9 +632,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,
)
err := circuit.ErrorEncrypter.Reextract(cm.cfg.ExtractSharedSecret)
if err != nil {
return nil, err
}
Expand Down
31 changes: 16 additions & 15 deletions htlcswitch/circuit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,17 @@ 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.
Expand Down Expand Up @@ -106,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 {
Expand Down Expand Up @@ -216,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 "+
Expand Down Expand Up @@ -643,11 +644,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")
Expand Down
64 changes: 53 additions & 11 deletions htlcswitch/failure.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package htlcswitch
import (
"bytes"
"fmt"
"strings"

sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
Expand Down Expand Up @@ -92,6 +93,13 @@ type ForwardingError struct {
// be nil in the case where we fail to decode failure message sent by
// a peer.
msg lnwire.FailureMessage

// HoldTimes is an array of hold times (in ms) as reported from the
// nodes of the route. It is the time for which a node held the HTLC for
// from that nodes local perspective. The first element corresponds to
// the first node after the sender node, with greater indices indicating
// nodes further down the route.
HoldTimes []uint32
}

// WireMessage extracts a valid wire failure message from an internal
Expand All @@ -116,11 +124,12 @@ func (f *ForwardingError) Error() string {
// NewForwardingError creates a new payment error which wraps a wire error
// with additional metadata.
func NewForwardingError(failure lnwire.FailureMessage,
index int) *ForwardingError {
index int, holdTimes []uint32) *ForwardingError {

return &ForwardingError{
FailureSourceIdx: index,
msg: failure,
HoldTimes: holdTimes,
}
}

Expand All @@ -140,7 +149,7 @@ type ErrorDecrypter interface {
// hop, to the source of the error. A fully populated
// lnwire.FailureMessage is returned along with the source of the
// error.
DecryptError(lnwire.OpaqueReason) (*ForwardingError, error)
DecryptError(lnwire.OpaqueReason, []byte) (*ForwardingError, error)
}

// UnknownEncrypterType is an error message used to signal that an unexpected
Expand All @@ -160,37 +169,70 @@ type OnionErrorDecrypter interface {
// node where error have occurred. As a result, in order to decrypt the
// error we need get all shared secret and apply decryption in the
// reverse order.
DecryptError(encryptedData []byte) (*sphinx.DecryptedError, error)
DecryptError(encryptedData, attrData []byte) (*sphinx.DecryptedError,
error)
}

// SphinxErrorDecrypter wraps the sphinx data SphinxErrorDecrypter and maps the
// returned errors to concrete lnwire.FailureMessage instances.
type SphinxErrorDecrypter struct {
OnionErrorDecrypter
decrypter *sphinx.OnionErrorDecrypter
}

// NewSphinxErrorDecrypter instantiates a new error decrypter.
func NewSphinxErrorDecrypter(circuit *sphinx.Circuit) *SphinxErrorDecrypter {
return &SphinxErrorDecrypter{
decrypter: sphinx.NewOnionErrorDecrypter(
circuit, hop.AttrErrorStruct,
),
}
}

// DecryptError peels off each layer of onion encryption from the first hop, to
// the source of the error. A fully populated lnwire.FailureMessage is returned
// along with the source of the error.
//
// NOTE: Part of the ErrorDecrypter interface.
func (s *SphinxErrorDecrypter) DecryptError(reason lnwire.OpaqueReason) (
*ForwardingError, error) {

failure, err := s.OnionErrorDecrypter.DecryptError(reason)
func (s *SphinxErrorDecrypter) DecryptError(reason lnwire.OpaqueReason,
attrData []byte) (*ForwardingError, error) {

// We do not set the strict attribution flag, as we want to account for
// the grace period during which nodes are still upgrading to support
// this feature. If set prematurely it can lead to early blame of our
// direct peers that may not support this feature yet, blacklisting our
// channels and failing our payments.
attrErr, err := s.decrypter.DecryptError(reason, attrData, false)
if err != nil {
return nil, err
}

var holdTimes []string
for _, payload := range attrErr.HoldTimes {
// Read hold time.
holdTime := payload

holdTimes = append(
holdTimes,
fmt.Sprintf("%vms", holdTime*100),
)
}

// For now just log the hold times, the collector of the payment result
// should handle this in a more sophisticated way.
log.Debugf("Extracted hold times from onion error: %v",
strings.Join(holdTimes, "/"))

// Decode the failure. If an error occurs, we leave the failure message
// field nil.
r := bytes.NewReader(failure.Message)
r := bytes.NewReader(attrErr.Message)
failureMsg, err := lnwire.DecodeFailure(r, 0)
if err != nil {
return NewUnknownForwardingError(failure.SenderIdx), nil
return NewUnknownForwardingError(attrErr.SenderIdx), nil
}

return NewForwardingError(failureMsg, failure.SenderIdx), nil
return NewForwardingError(
failureMsg, attrErr.SenderIdx, attrErr.HoldTimes,
), nil
}

// A compile time check to ensure ErrorDecrypter implements the Deobfuscator
Expand Down
7 changes: 5 additions & 2 deletions htlcswitch/failure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/btcsuite/btcd/btcec/v2"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -52,11 +53,13 @@ func TestLongFailureMessage(t *testing.T) {
}

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

// Assert that the failure message can still be extracted.
failure, err := errorDecryptor.DecryptError(reason)
failure, err := errorDecryptor.DecryptError(reason, nil)
require.NoError(t, err)

incorrectDetails, ok := failure.msg.(*lnwire.FailIncorrectDetails)
Expand Down
Loading
Loading