Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

htlcswitch: attributable errors #7139

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions feature/default_sets.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,9 @@ var defaultSetDesc = setDesc{
SetInit: {}, // I
SetNodeAnn: {}, // N
},
lnwire.AttributableErrorsOptional: {
SetInit: {}, // I
SetNodeAnn: {}, // N
SetInvoice: {}, // 9
},
}
19 changes: 18 additions & 1 deletion htlcswitch/circuit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
148 changes: 135 additions & 13 deletions htlcswitch/hop/error_encryptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
joostjager marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -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{}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming we'll want an encrypter specific interface here, even if it's just a marker one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it useful to define a marker interface in golang other than for documenting this specific field? I think it doesn't actually provide any compiler safety because every type will satisfy the interface?


EphemeralKey *btcec.PublicKey
CreatedAt time.Time

attrError bool
}

// NewSphinxErrorEncrypterUninitialized initializes a blank sphinx error
Expand All @@ -94,8 +121,7 @@ type SphinxErrorEncrypter struct {
// OnionProcessor.
func NewSphinxErrorEncrypterUninitialized() *SphinxErrorEncrypter {
return &SphinxErrorEncrypter{
OnionErrorEncrypter: nil,
EphemeralKey: &btcec.PublicKey{},
EphemeralKey: &btcec.PublicKey{},
}
}

Expand All @@ -104,10 +130,18 @@ func NewSphinxErrorEncrypterUninitialized() *SphinxErrorEncrypter {
// SphinxErrorEncrypter, use the NewSphinxErrorEncrypterUninitialized
// constructor.
func NewSphinxErrorEncrypter(ephemeralKey *btcec.PublicKey,
joostjager marked this conversation as resolved.
Show resolved Hide resolved
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)
Copy link
Contributor

@positiveblue positiveblue May 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think time.Now() already returns the a value in nanoseconds (which happens to be the max granularity in go too)

Also, given that we will return the hold time in ms and some databases cannot have a max granularity for timestamps of microseconds (I am thinking about postgres) should we truncate to micro/miliseconds here? If not we can always store it as int64

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a reason why I had to do this. I think it was to strip off the monotonic clock reading.

}

encrypter.initialize(sharedSecret)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the incentives for a router to report back honestly and to not to pass on a short error? Will this node be penalized for reporting? If the node does not get penalized for reporting, any relayer could do this to penalize their neighbor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they pass the short error, the upstream node will replace it by a proper error and the sender will indeed penalize this 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we could export minPaddedOnionErrorLength and insert it here. So semantically this would tell us that we have a zero message length and zero padding?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, although in the rest of the lnd repo we generally calculate that number 260 from lnwire.FailureMessageLength and I aligned this new code with that.

Semantically we'll indeed communicate zero message with zero padding. But it really does not matter what we put here.

As a future step, we could define a new failure code in BOLT 04 for this, but it won't change much about the penalization.


return s.encrypt(true, reason)

case err != nil:
return lnwire.OpaqueReason{}, err
}

return encrypted, nil
}

// Type returns the identifier for a sphinx error encrypter.
Expand All @@ -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
Expand All @@ -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
}

Expand Down
33 changes: 23 additions & 10 deletions htlcswitch/hop/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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
}

Expand Down
16 changes: 13 additions & 3 deletions htlcswitch/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Copy link
Contributor Author

@joostjager joostjager Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallback to legacy format is probably fine. If this happens to be an attributable error route, the upstream node will substitute with an attributable error?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be added to the spec.

obfuscator := l.cfg.CreateErrorEncrypter(
ephemeralKey, sharedSecret,
ephemeralKey, sharedSecret, false,
)

heightNow := l.cfg.BestHeight()
Expand Down Expand Up @@ -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 {
Expand Down
Loading