Merge pull request #3442 from cfromknecht/router-registry

single-shot, sender-side mpp via sendtoroute
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2019-11-04 17:50:59 -08:00
commit 7ecb7136cf
28 files changed
+1782 -770

No files matched your search

+41 -2
View File
@@ -14,6 +14,7 @@ import (
"github.com/coreos/bbolt" "github.com/coreos/bbolt"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/tlv" "github.com/lightningnetwork/lnd/tlv"
) )
@@ -508,7 +509,9 @@ func deserializePaymentAttemptInfo(r io.Reader) (*PaymentAttemptInfo, error) {
func serializeHop(w io.Writer, h *route.Hop) error { func serializeHop(w io.Writer, h *route.Hop) error {
if err := WriteElements(w, if err := WriteElements(w,
h.PubKeyBytes[:], h.ChannelID, h.OutgoingTimeLock, h.PubKeyBytes[:],
h.ChannelID,
h.OutgoingTimeLock,
h.AmtToForward, h.AmtToForward,
); err != nil { ); err != nil {
return err return err
@@ -525,10 +528,23 @@ func serializeHop(w io.Writer, h *route.Hop) error {
return WriteElements(w, uint32(0)) return WriteElements(w, uint32(0))
} }
// Gather all non-primitive TLV records so that they can be serialized
// as a single blob.
//
// TODO(conner): add migration to unify all fields in a single TLV
// blobs. The split approach will cause headaches down the road as more
// fields are added, which we can avoid by having a single TLV stream
// for all payload fields.
var records []tlv.Record
if h.MPP != nil {
records = append(records, h.MPP.Record())
}
records = append(records, h.TLVRecords...)
// Otherwise, we'll transform our slice of records into a map of the // Otherwise, we'll transform our slice of records into a map of the
// raw bytes, then serialize them in-line with a length (number of // raw bytes, then serialize them in-line with a length (number of
// elements) prefix. // elements) prefix.
mapRecords, err := tlv.RecordsToMap(h.TLVRecords) mapRecords, err := tlv.RecordsToMap(records)
if err != nil { if err != nil {
return err return err
} }
@@ -604,6 +620,29 @@ func deserializeHop(r io.Reader) (*route.Hop, error) {
tlvMap[tlvType] = rawRecordBytes tlvMap[tlvType] = rawRecordBytes
} }
// If the MPP type is present, remove it from the generic TLV map and
// parse it back into a proper MPP struct.
//
// TODO(conner): add migration to unify all fields in a single TLV
// blobs. The split approach will cause headaches down the road as more
// fields are added, which we can avoid by having a single TLV stream
// for all payload fields.
mppType := uint64(record.MPPOnionType)
if mppBytes, ok := tlvMap[mppType]; ok {
delete(tlvMap, mppType)
var (
mpp = &record.MPP{}
mppRec = mpp.Record()
r = bytes.NewReader(mppBytes)
)
err := mppRec.Decode(r, uint64(len(mppBytes)))
if err != nil {
return nil, err
}
h.MPP = mpp
}
tlvRecords, err := tlv.MapToRecords(tlvMap) tlvRecords, err := tlv.MapToRecords(tlvMap)
if err != nil { if err != nil {
return nil, err return nil, err
+3 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/btcec"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/tlv" "github.com/lightningnetwork/lnd/tlv"
) )
@@ -31,6 +32,7 @@ var (
tlv.MakeStaticRecord(1, nil, 3, tlvEncoder, nil), tlv.MakeStaticRecord(1, nil, 3, tlvEncoder, nil),
tlv.MakeStaticRecord(2, nil, 3, tlvEncoder, nil), tlv.MakeStaticRecord(2, nil, 3, tlvEncoder, nil),
}, },
MPP: record.NewMPP(32, [32]byte{0x42}),
} }
testHop2 = &route.Hop{ testHop2 = &route.Hop{
@@ -46,8 +48,8 @@ var (
TotalAmount: 1234567, TotalAmount: 1234567,
SourcePubKey: route.NewVertex(pub), SourcePubKey: route.NewVertex(pub),
Hops: []*route.Hop{ Hops: []*route.Hop{
testHop1,
testHop2, testHop2,
testHop1,
}, },
} }
) )
+1 -1
View File
@@ -21,7 +21,7 @@ type Registry interface {
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi, NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
expiry uint32, currentHeight int32, expiry uint32, currentHeight int32,
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{}, circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*invoices.HodlEvent, error) payload invoices.Payload) (*invoices.HodlEvent, error)
// HodlUnsubscribeAll unsubscribes from all hodl events. // HodlUnsubscribeAll unsubscribes from all hodl events.
HodlUnsubscribeAll(subscriber chan<- interface{}) HodlUnsubscribeAll(subscriber chan<- interface{})
+1 -1
View File
@@ -24,7 +24,7 @@ type mockRegistry struct {
func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash, func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32, paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{}, circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*invoices.HodlEvent, error) { payload invoices.Payload) (*invoices.HodlEvent, error) {
r.notifyChan <- notifyExitHopData{ r.notifyChan <- notifyExitHopData{
hodlChan: hodlChan, hodlChan: hodlChan,
+17 -34
View File
@@ -16,15 +16,13 @@ import (
// interpret the forwarding information encoded within the HTLC packet, and hop // interpret the forwarding information encoded within the HTLC packet, and hop
// to encode the forwarding information for the _next_ hop. // to encode the forwarding information for the _next_ hop.
type Iterator interface { type Iterator interface {
// ForwardingInstructions returns the set of fields that detail exactly // HopPayload returns the set of fields that detail exactly _how_ this
// _how_ this hop should forward the HTLC to the next hop. // hop should forward the HTLC to the next hop. Additionally, the
// Additionally, the information encoded within the returned // information encoded within the returned ForwardingInfo is to be used
// ForwardingInfo is to be used by each hop to authenticate the // by each hop to authenticate the information given to it by the prior
// information given to it by the prior hop. // hop. The payload will also contain any additional TLV fields provided
ForwardingInstructions() (ForwardingInfo, error) // by the sender.
HopPayload() (*Payload, error)
// ExtraOnionBlob returns the additional EOB data (if available).
ExtraOnionBlob() []byte
// EncodeNextHop encodes the onion packet destined for the next hop // EncodeNextHop encodes the onion packet destined for the next hop
// into the passed io.Writer. // into the passed io.Writer.
@@ -72,50 +70,35 @@ func (r *sphinxHopIterator) EncodeNextHop(w io.Writer) error {
return r.processedPacket.NextPacket.Encode(w) return r.processedPacket.NextPacket.Encode(w)
} }
// ForwardingInstructions returns the set of fields that detail exactly _how_ // HopPayload returns the set of fields that detail exactly _how_ this hop
// this hop should forward the HTLC to the next hop. Additionally, the // should forward the HTLC to the next hop. Additionally, the information
// information encoded within the returned ForwardingInfo is to be used by each // encoded within the returned ForwardingInfo is to be used by each hop to
// hop to authenticate the information given to it by the prior hop. // authenticate the information given to it by the prior hop. The payload will
// also contain any additional TLV fields provided by the sender.
// //
// NOTE: Part of the HopIterator interface. // NOTE: Part of the HopIterator interface.
func (r *sphinxHopIterator) ForwardingInstructions() (ForwardingInfo, error) { func (r *sphinxHopIterator) HopPayload() (*Payload, error) {
switch r.processedPacket.Payload.Type { switch r.processedPacket.Payload.Type {
// If this is the legacy payload, then we'll extract the information // If this is the legacy payload, then we'll extract the information
// directly from the pre-populated ForwardingInstructions field. // directly from the pre-populated ForwardingInstructions field.
case sphinx.PayloadLegacy: case sphinx.PayloadLegacy:
fwdInst := r.processedPacket.ForwardingInstructions fwdInst := r.processedPacket.ForwardingInstructions
p := NewLegacyPayload(fwdInst) return NewLegacyPayload(fwdInst), nil
return p.ForwardingInfo(), nil
// Otherwise, if this is the TLV payload, then we'll make a new stream // Otherwise, if this is the TLV payload, then we'll make a new stream
// to decode only what we need to make routing decisions. // to decode only what we need to make routing decisions.
case sphinx.PayloadTLV: case sphinx.PayloadTLV:
p, err := NewPayloadFromReader(bytes.NewReader( return NewPayloadFromReader(bytes.NewReader(
r.processedPacket.Payload.Payload, r.processedPacket.Payload.Payload,
)) ))
if err != nil {
return ForwardingInfo{}, err
}
return p.ForwardingInfo(), nil
default: default:
return ForwardingInfo{}, fmt.Errorf("unknown "+ return nil, fmt.Errorf("unknown sphinx payload type: %v",
"sphinx payload type: %v",
r.processedPacket.Payload.Type) r.processedPacket.Payload.Type)
} }
} }
// ExtraOnionBlob returns the additional EOB data (if available).
func (r *sphinxHopIterator) ExtraOnionBlob() []byte {
if r.processedPacket.Payload.Type == sphinx.PayloadLegacy {
return nil
}
return r.processedPacket.Payload.Payload
}
// ExtractErrorEncrypter decodes and returns the ErrorEncrypter for this hop, // ExtractErrorEncrypter decodes and returns the ErrorEncrypter for this hop,
// along with a failure code to signal if the decoding was successful. The // 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 // ErrorEncrypter is used to encrypt errors back to the sender in the event that
+2 -1
View File
@@ -85,12 +85,13 @@ func TestSphinxHopIteratorForwardingInstructions(t *testing.T) {
for i, testCase := range testCases { for i, testCase := range testCases {
iterator.processedPacket = testCase.sphinxPacket iterator.processedPacket = testCase.sphinxPacket
fwdInfo, err := iterator.ForwardingInstructions() pld, err := iterator.HopPayload()
if err != nil { if err != nil {
t.Fatalf("#%v: unable to extract forwarding "+ t.Fatalf("#%v: unable to extract forwarding "+
"instructions: %v", i, err) "instructions: %v", i, err)
} }
fwdInfo := pld.ForwardingInfo()
if fwdInfo != testCase.expectedFwdInfo { if fwdInfo != testCase.expectedFwdInfo {
t.Fatalf("#%v: wrong fwding info: expected %v, got %v", t.Fatalf("#%v: wrong fwding info: expected %v, got %v",
i, spew.Sdump(testCase.expectedFwdInfo), i, spew.Sdump(testCase.expectedFwdInfo),
+28
View File
@@ -81,6 +81,10 @@ type Payload struct {
// FwdInfo holds the basic parameters required for HTLC forwarding, e.g. // FwdInfo holds the basic parameters required for HTLC forwarding, e.g.
// amount, cltv, and next hop. // amount, cltv, and next hop.
FwdInfo ForwardingInfo FwdInfo ForwardingInfo
// MPP holds the info provided in an option_mpp record when parsed from
// a TLV onion payload.
MPP *record.MPP
} }
// NewLegacyPayload builds a Payload from the amount, cltv, and next hop // NewLegacyPayload builds a Payload from the amount, cltv, and next hop
@@ -105,12 +109,14 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
cid uint64 cid uint64
amt uint64 amt uint64
cltv uint32 cltv uint32
mpp = &record.MPP{}
) )
tlvStream, err := tlv.NewStream( tlvStream, err := tlv.NewStream(
record.NewAmtToFwdRecord(&amt), record.NewAmtToFwdRecord(&amt),
record.NewLockTimeRecord(&cltv), record.NewLockTimeRecord(&cltv),
record.NewNextHopIDRecord(&cid), record.NewNextHopIDRecord(&cid),
mpp.Record(),
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -151,6 +157,12 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
return nil, err return nil, err
} }
// If no MPP field was parsed, set the MPP field on the resulting
// payload to nil.
if _, ok := parsedTypes[record.MPPOnionType]; !ok {
mpp = nil
}
return &Payload{ return &Payload{
FwdInfo: ForwardingInfo{ FwdInfo: ForwardingInfo{
Network: BitcoinNetwork, Network: BitcoinNetwork,
@@ -158,6 +170,7 @@ func NewPayloadFromReader(r io.Reader) (*Payload, error) {
AmountToForward: lnwire.MilliSatoshi(amt), AmountToForward: lnwire.MilliSatoshi(amt),
OutgoingCTLV: cltv, OutgoingCTLV: cltv,
}, },
MPP: mpp,
}, nil }, nil
} }
@@ -179,6 +192,7 @@ func ValidateParsedPayloadTypes(parsedTypes tlv.TypeSet,
_, hasAmt := parsedTypes[record.AmtOnionType] _, hasAmt := parsedTypes[record.AmtOnionType]
_, hasLockTime := parsedTypes[record.LockTimeOnionType] _, hasLockTime := parsedTypes[record.LockTimeOnionType]
_, hasNextHop := parsedTypes[record.NextHopOnionType] _, hasNextHop := parsedTypes[record.NextHopOnionType]
_, hasMPP := parsedTypes[record.MPPOnionType]
switch { switch {
@@ -207,7 +221,21 @@ func ValidateParsedPayloadTypes(parsedTypes tlv.TypeSet,
Violation: IncludedViolation, Violation: IncludedViolation,
FinalHop: true, FinalHop: true,
} }
// Intermediate nodes should never receive MPP fields.
case !isFinalHop && hasMPP:
return ErrInvalidPayload{
Type: record.MPPOnionType,
Violation: IncludedViolation,
FinalHop: isFinalHop,
}
} }
return nil return nil
} }
// MultiPath returns the record corresponding the option_mpp parsed from the
// onion payload.
func (h *Payload) MultiPath() *record.MPP {
return h.MPP
}
+90 -6
View File
@@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/lightningnetwork/lnd/htlcswitch/hop" "github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/record"
) )
@@ -13,6 +14,7 @@ type decodePayloadTest struct {
name string name string
payload []byte payload []byte
expErr error expErr error
shouldHaveMPP bool
} }
var decodePayloadTests = []decodePayloadTest{ var decodePayloadTests = []decodePayloadTest{
@@ -79,9 +81,9 @@ var decodePayloadTests = []decodePayloadTest{
}, },
{ {
name: "required type after omitted hop id", name: "required type after omitted hop id",
payload: []byte{0x02, 0x00, 0x04, 0x00, 0x08, 0x00}, payload: []byte{0x02, 0x00, 0x04, 0x00, 0x0a, 0x00},
expErr: hop.ErrInvalidPayload{ expErr: hop.ErrInvalidPayload{
Type: 8, Type: 10,
Violation: hop.RequiredViolation, Violation: hop.RequiredViolation,
FinalHop: true, FinalHop: true,
}, },
@@ -89,10 +91,10 @@ var decodePayloadTests = []decodePayloadTest{
{ {
name: "required type after included hop id", name: "required type after included hop id",
payload: []byte{0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x01, 0x00, payload: []byte{0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00,
}, },
expErr: hop.ErrInvalidPayload{ expErr: hop.ErrInvalidPayload{
Type: 8, Type: 10,
Violation: hop.RequiredViolation, Violation: hop.RequiredViolation,
FinalHop: false, FinalHop: false,
}, },
@@ -112,7 +114,7 @@ var decodePayloadTests = []decodePayloadTest{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}, },
expErr: hop.ErrInvalidPayload{ expErr: hop.ErrInvalidPayload{
Type: 6, Type: record.NextHopOnionType,
Violation: hop.IncludedViolation, Violation: hop.IncludedViolation,
FinalHop: true, FinalHop: true,
}, },
@@ -128,6 +130,60 @@ var decodePayloadTests = []decodePayloadTest{
FinalHop: false, FinalHop: false,
}, },
}, },
{
name: "valid intermediate hop",
payload: []byte{0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
expErr: nil,
},
{
name: "valid final hop",
payload: []byte{0x02, 0x00, 0x04, 0x00},
expErr: nil,
},
{
name: "intermediate hop with mpp",
payload: []byte{
// amount
0x02, 0x00,
// cltv
0x04, 0x00,
// next hop id
0x06, 0x08,
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// mpp
0x08, 0x21,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x08,
},
expErr: hop.ErrInvalidPayload{
Type: record.MPPOnionType,
Violation: hop.IncludedViolation,
FinalHop: false,
},
},
{
name: "final hop with mpp",
payload: []byte{
// amount
0x02, 0x00,
// cltv
0x04, 0x00,
// mpp
0x08, 0x21,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x08,
},
expErr: nil,
shouldHaveMPP: true,
},
} }
// TestDecodeHopPayloadRecordValidation asserts that parsing the payloads in the // TestDecodeHopPayloadRecordValidation asserts that parsing the payloads in the
@@ -142,9 +198,37 @@ func TestDecodeHopPayloadRecordValidation(t *testing.T) {
} }
func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) { func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
_, err := hop.NewPayloadFromReader(bytes.NewReader(test.payload)) var (
testTotalMsat = lnwire.MilliSatoshi(8)
testAddr = [32]byte{
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
}
)
p, err := hop.NewPayloadFromReader(bytes.NewReader(test.payload))
if !reflect.DeepEqual(test.expErr, err) { if !reflect.DeepEqual(test.expErr, err) {
t.Fatalf("expected error mismatch, want: %v, got: %v", t.Fatalf("expected error mismatch, want: %v, got: %v",
test.expErr, err) test.expErr, err)
} }
if err != nil {
return
}
// Assert MPP fields if we expect them.
if test.shouldHaveMPP {
if p.MPP == nil {
t.Fatalf("payload should have MPP record")
}
if p.MPP.TotalMsat() != testTotalMsat {
t.Fatalf("invalid total msat")
}
if p.MPP.PaymentAddr() != testAddr {
t.Fatalf("invalid payment addr")
}
} else if p.MPP != nil {
t.Fatalf("unexpected MPP payload")
}
} }
+1 -1
View File
@@ -27,7 +27,7 @@ type InvoiceDatabase interface {
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi, NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
expiry uint32, currentHeight int32, expiry uint32, currentHeight int32,
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{}, circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*invoices.HodlEvent, error) payload invoices.Payload) (*invoices.HodlEvent, error)
// CancelInvoice attempts to cancel the invoice corresponding to the // CancelInvoice attempts to cancel the invoice corresponding to the
// passed payment hash. // passed payment hash.
+6 -5
View File
@@ -2642,7 +2642,7 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
heightNow := l.cfg.Switch.BestHeight() heightNow := l.cfg.Switch.BestHeight()
fwdInfo, err := chanIterator.ForwardingInstructions() pld, err := chanIterator.HopPayload()
if err != nil { if err != nil {
// If we're unable to process the onion payload, or we // If we're unable to process the onion payload, or we
// received invalid onion payload failure, then we // received invalid onion payload failure, then we
@@ -2671,11 +2671,12 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
continue continue
} }
fwdInfo := pld.ForwardingInfo()
switch fwdInfo.NextHop { switch fwdInfo.NextHop {
case hop.Exit: case hop.Exit:
updated, err := l.processExitHop( updated, err := l.processExitHop(
pd, obfuscator, fwdInfo, heightNow, pd, obfuscator, fwdInfo, heightNow, pld,
chanIterator.ExtraOnionBlob(),
) )
if err != nil { if err != nil {
l.fail(LinkFailureError{code: ErrInternalError}, l.fail(LinkFailureError{code: ErrInternalError},
@@ -2844,7 +2845,7 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
// returns a boolean indicating whether the commitment tx needs an update. // returns a boolean indicating whether the commitment tx needs an update.
func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor, func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
obfuscator hop.ErrorEncrypter, fwdInfo hop.ForwardingInfo, obfuscator hop.ErrorEncrypter, fwdInfo hop.ForwardingInfo,
heightNow uint32, eob []byte) (bool, error) { heightNow uint32, payload invoices.Payload) (bool, error) {
// If hodl.ExitSettle is requested, we will not validate the final hop's // If hodl.ExitSettle is requested, we will not validate the final hop's
// ADD, nor will we settle the corresponding invoice or respond with the // ADD, nor will we settle the corresponding invoice or respond with the
@@ -2895,7 +2896,7 @@ func (l *channelLink) processExitHop(pd *lnwallet.PaymentDescriptor,
event, err := l.cfg.Registry.NotifyExitHopHtlc( event, err := l.cfg.Registry.NotifyExitHopHtlc(
invoiceHash, pd.Amount, pd.Timeout, int32(heightNow), invoiceHash, pd.Amount, pd.Timeout, int32(heightNow),
circuitKey, l.hodlQueue.ChanIn(), eob, circuitKey, l.hodlQueue.ChanIn(), payload,
) )
switch err { switch err {
+10 -9
View File
@@ -22,6 +22,7 @@ import (
"github.com/coreos/bbolt" "github.com/coreos/bbolt"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/go-errors/errors" "github.com/go-errors/errors"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/contractcourt" "github.com/lightningnetwork/lnd/contractcourt"
@@ -563,7 +564,7 @@ func TestExitNodeTimelockPayloadMismatch(t *testing.T) {
// per-hop payload for outgoing time lock to be the incorrect value. // per-hop payload for outgoing time lock to be the incorrect value.
// The proper value of the outgoing CLTV should be the policy set by // The proper value of the outgoing CLTV should be the policy set by
// the receiving node, instead we set it to be a random value. // the receiving node, instead we set it to be a random value.
hops[0].OutgoingCTLV = 500 hops[0].FwdInfo.OutgoingCTLV = 500
firstHop := n.firstBobChannelLink.ShortChanID() firstHop := n.firstBobChannelLink.ShortChanID()
_, err = makePayment( _, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt, n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt,
@@ -616,7 +617,7 @@ func TestExitNodeAmountPayloadMismatch(t *testing.T) {
// per-hop payload for amount to be the incorrect value. The proper // per-hop payload for amount to be the incorrect value. The proper
// value of the amount to forward should be the amount that the // value of the amount to forward should be the amount that the
// receiving node expects to receive. // receiving node expects to receive.
hops[0].AmountToForward = 1 hops[0].FwdInfo.AmountToForward = 1
firstHop := n.firstBobChannelLink.ShortChanID() firstHop := n.firstBobChannelLink.ShortChanID()
_, err = makePayment( _, err = makePayment(
n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt, n.aliceServer, n.bobServer, firstHop, hops, amount, htlcAmt,
@@ -4354,13 +4355,13 @@ func generateHtlcAndInvoice(t *testing.T,
htlcAmt := lnwire.NewMSatFromSatoshis(10000) htlcAmt := lnwire.NewMSatFromSatoshis(10000)
htlcExpiry := testStartingHeight + testInvoiceCltvExpiry htlcExpiry := testStartingHeight + testInvoiceCltvExpiry
hops := []hop.ForwardingInfo{ hops := []*hop.Payload{
{ hop.NewLegacyPayload(&sphinx.HopData{
Network: hop.BitcoinNetwork, Realm: [1]byte{}, // hop.BitcoinNetwork
NextHop: hop.Exit, NextAddress: [8]byte{}, // hop.Exit,
AmountToForward: htlcAmt, ForwardAmount: uint64(htlcAmt),
OutgoingCTLV: uint32(htlcExpiry), OutgoingCltv: uint32(htlcExpiry),
}, }),
} }
blob, err := generateRoute(hops...) blob, err := generateRoute(hops...)
if err != nil { if err != nil {
+20 -12
View File
@@ -265,16 +265,14 @@ func (s *mockServer) QuitSignal() <-chan struct{} {
// mockHopIterator represents the test version of hop iterator which instead // mockHopIterator represents the test version of hop iterator which instead
// of encrypting the path in onion blob just stores the path as a list of hops. // of encrypting the path in onion blob just stores the path as a list of hops.
type mockHopIterator struct { type mockHopIterator struct {
hops []hop.ForwardingInfo hops []*hop.Payload
} }
func newMockHopIterator(hops ...hop.ForwardingInfo) hop.Iterator { func newMockHopIterator(hops ...*hop.Payload) hop.Iterator {
return &mockHopIterator{hops: hops} return &mockHopIterator{hops: hops}
} }
func (r *mockHopIterator) ForwardingInstructions() ( func (r *mockHopIterator) HopPayload() (*hop.Payload, error) {
hop.ForwardingInfo, error) {
h := r.hops[0] h := r.hops[0]
r.hops = r.hops[1:] r.hops = r.hops[1:]
return h, nil return h, nil
@@ -300,7 +298,8 @@ func (r *mockHopIterator) EncodeNextHop(w io.Writer) error {
} }
for _, hop := range r.hops { for _, hop := range r.hops {
if err := encodeFwdInfo(w, &hop); err != nil { fwdInfo := hop.ForwardingInfo()
if err := encodeFwdInfo(w, &fwdInfo); err != nil {
return err return err
} }
} }
@@ -434,14 +433,22 @@ func (p *mockIteratorDecoder) DecodeHopIterator(r io.Reader, rHash []byte,
} }
hopLength := binary.BigEndian.Uint32(b[:]) hopLength := binary.BigEndian.Uint32(b[:])
hops := make([]hop.ForwardingInfo, hopLength) hops := make([]*hop.Payload, hopLength)
for i := uint32(0); i < hopLength; i++ { for i := uint32(0); i < hopLength; i++ {
f := &hop.ForwardingInfo{} var f hop.ForwardingInfo
if err := decodeFwdInfo(r, f); err != nil { if err := decodeFwdInfo(r, &f); err != nil {
return nil, lnwire.CodeTemporaryChannelFailure return nil, lnwire.CodeTemporaryChannelFailure
} }
hops[i] = *f var nextHopBytes [8]byte
binary.BigEndian.PutUint64(nextHopBytes[:], f.NextHop.ToUint64())
hops[i] = hop.NewLegacyPayload(&sphinx.HopData{
Realm: [1]byte{}, // hop.BitcoinNetwork
NextAddress: nextHopBytes,
ForwardAmount: uint64(f.AmountToForward),
OutgoingCltv: f.OutgoingCTLV,
})
} }
return newMockHopIterator(hops...), lnwire.CodeNone return newMockHopIterator(hops...), lnwire.CodeNone
@@ -807,10 +814,11 @@ func (i *mockInvoiceRegistry) SettleHodlInvoice(preimage lntypes.Preimage) error
func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash, func (i *mockInvoiceRegistry) NotifyExitHopHtlc(rhash lntypes.Hash,
amt lnwire.MilliSatoshi, expiry uint32, currentHeight int32, amt lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{}, circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*invoices.HodlEvent, error) { payload invoices.Payload) (*invoices.HodlEvent, error) {
event, err := i.registry.NotifyExitHopHtlc( event, err := i.registry.NotifyExitHopHtlc(
rhash, amt, expiry, currentHeight, circuitKey, hodlChan, eob, rhash, amt, expiry, currentHeight, circuitKey, hodlChan,
payload,
) )
if err != nil { if err != nil {
return nil, err return nil, err
+17 -14
View File
@@ -23,6 +23,7 @@ import (
"github.com/btcsuite/fastsha256" "github.com/btcsuite/fastsha256"
"github.com/coreos/bbolt" "github.com/coreos/bbolt"
"github.com/go-errors/errors" "github.com/go-errors/errors"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/contractcourt" "github.com/lightningnetwork/lnd/contractcourt"
"github.com/lightningnetwork/lnd/htlcswitch/hop" "github.com/lightningnetwork/lnd/htlcswitch/hop"
@@ -601,7 +602,7 @@ func generatePayment(invoiceAmt, htlcAmt lnwire.MilliSatoshi, timelock uint32,
} }
// generateRoute generates the path blob by given array of peers. // generateRoute generates the path blob by given array of peers.
func generateRoute(hops ...hop.ForwardingInfo) ( func generateRoute(hops ...*hop.Payload) (
[lnwire.OnionPacketSize]byte, error) { [lnwire.OnionPacketSize]byte, error) {
var blob [lnwire.OnionPacketSize]byte var blob [lnwire.OnionPacketSize]byte
@@ -642,13 +643,12 @@ type threeHopNetwork struct {
// also the time lock value needed to route an HTLC with the target amount over // also the time lock value needed to route an HTLC with the target amount over
// the specified path. // the specified path.
func generateHops(payAmt lnwire.MilliSatoshi, startingHeight uint32, func generateHops(payAmt lnwire.MilliSatoshi, startingHeight uint32,
path ...*channelLink) (lnwire.MilliSatoshi, uint32, path ...*channelLink) (lnwire.MilliSatoshi, uint32, []*hop.Payload) {
[]hop.ForwardingInfo) {
totalTimelock := startingHeight totalTimelock := startingHeight
runningAmt := payAmt runningAmt := payAmt
hops := make([]hop.ForwardingInfo, len(path)) hops := make([]*hop.Payload, len(path))
for i := len(path) - 1; i >= 0; i-- { for i := len(path) - 1; i >= 0; i-- {
// If this is the last hop, then the next hop is the special // If this is the last hop, then the next hop is the special
// "exit node". Otherwise, we look to the "prior" hop. // "exit node". Otherwise, we look to the "prior" hop.
@@ -676,7 +676,7 @@ func generateHops(payAmt lnwire.MilliSatoshi, startingHeight uint32,
amount := payAmt amount := payAmt
if i != len(path)-1 { if i != len(path)-1 {
prevHop := hops[i+1] prevHop := hops[i+1]
prevAmount := prevHop.AmountToForward prevAmount := prevHop.ForwardingInfo().AmountToForward
fee := ExpectedFee(path[i].cfg.FwrdingPolicy, prevAmount) fee := ExpectedFee(path[i].cfg.FwrdingPolicy, prevAmount)
runningAmt += fee runningAmt += fee
@@ -687,12 +687,15 @@ func generateHops(payAmt lnwire.MilliSatoshi, startingHeight uint32,
amount = runningAmt - fee amount = runningAmt - fee
} }
hops[i] = hop.ForwardingInfo{ var nextHopBytes [8]byte
Network: hop.BitcoinNetwork, binary.BigEndian.PutUint64(nextHopBytes[:], nextHop.ToUint64())
NextHop: nextHop,
AmountToForward: amount, hops[i] = hop.NewLegacyPayload(&sphinx.HopData{
OutgoingCTLV: timeLock, Realm: [1]byte{}, // hop.BitcoinNetwork
} NextAddress: nextHopBytes,
ForwardAmount: uint64(amount),
OutgoingCltv: timeLock,
})
} }
return runningAmt, totalTimelock, hops return runningAmt, totalTimelock, hops
@@ -739,7 +742,7 @@ func waitForPayFuncResult(payFunc func() error, d time.Duration) error {
// * from Alice to Carol through the Bob // * from Alice to Carol through the Bob
// * from Alice to some another peer through the Bob // * from Alice to some another peer through the Bob
func makePayment(sendingPeer, receivingPeer lnpeer.Peer, func makePayment(sendingPeer, receivingPeer lnpeer.Peer,
firstHop lnwire.ShortChannelID, hops []hop.ForwardingInfo, firstHop lnwire.ShortChannelID, hops []*hop.Payload,
invoiceAmt, htlcAmt lnwire.MilliSatoshi, invoiceAmt, htlcAmt lnwire.MilliSatoshi,
timelock uint32) *paymentResponse { timelock uint32) *paymentResponse {
@@ -773,7 +776,7 @@ func makePayment(sendingPeer, receivingPeer lnpeer.Peer,
// preparePayment creates an invoice at the receivingPeer and returns a function // preparePayment creates an invoice at the receivingPeer and returns a function
// that, when called, launches the payment from the sendingPeer. // that, when called, launches the payment from the sendingPeer.
func preparePayment(sendingPeer, receivingPeer lnpeer.Peer, func preparePayment(sendingPeer, receivingPeer lnpeer.Peer,
firstHop lnwire.ShortChannelID, hops []hop.ForwardingInfo, firstHop lnwire.ShortChannelID, hops []*hop.Payload,
invoiceAmt, htlcAmt lnwire.MilliSatoshi, invoiceAmt, htlcAmt lnwire.MilliSatoshi,
timelock uint32) (*channeldb.Invoice, func() error, error) { timelock uint32) (*channeldb.Invoice, func() error, error) {
@@ -1265,7 +1268,7 @@ func (n *twoHopNetwork) stop() {
} }
func (n *twoHopNetwork) makeHoldPayment(sendingPeer, receivingPeer lnpeer.Peer, func (n *twoHopNetwork) makeHoldPayment(sendingPeer, receivingPeer lnpeer.Peer,
firstHop lnwire.ShortChannelID, hops []hop.ForwardingInfo, firstHop lnwire.ShortChannelID, hops []*hop.Payload,
invoiceAmt, htlcAmt lnwire.MilliSatoshi, invoiceAmt, htlcAmt lnwire.MilliSatoshi,
timelock uint32, preimage lntypes.Preimage) chan error { timelock uint32, preimage lntypes.Preimage) chan error {
+11
View File
@@ -0,0 +1,11 @@
package invoices
import "github.com/lightningnetwork/lnd/record"
// Payload abstracts access to any additional fields provided in the final hop's
// TLV onion payload.
type Payload interface {
// MultiPath returns the record corresponding the option_mpp parsed from
// the onion payload.
MultiPath() *record.MPP
}
+1 -1
View File
@@ -429,7 +429,7 @@ func (i *InvoiceRegistry) LookupInvoice(rHash lntypes.Hash) (channeldb.Invoice,
func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash, func (i *InvoiceRegistry) NotifyExitHopHtlc(rHash lntypes.Hash,
amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32, amtPaid lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
circuitKey channeldb.CircuitKey, hodlChan chan<- interface{}, circuitKey channeldb.CircuitKey, hodlChan chan<- interface{},
eob []byte) (*HodlEvent, error) { payload Payload) (*HodlEvent, error) {
i.Lock() i.Lock()
defer i.Unlock() defer i.Unlock()
+68
View File
@@ -13,7 +13,9 @@ import (
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing" "github.com/lightningnetwork/lnd/routing"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/tlv" "github.com/lightningnetwork/lnd/tlv"
@@ -352,6 +354,17 @@ func (r *RouterBackend) MarshallRoute(route *route.Route) (*lnrpc.Route, error)
chanCapacity = incomingAmt.ToSatoshis() chanCapacity = incomingAmt.ToSatoshis()
} }
// Extract the MPP fields if present on this hop.
var mpp *lnrpc.MPPRecord
if hop.MPP != nil {
addr := hop.MPP.PaymentAddr()
mpp = &lnrpc.MPPRecord{
PaymentAddr: addr[:],
TotalAmtMsat: int64(hop.MPP.TotalMsat()),
}
}
resp.Hops[i] = &lnrpc.Hop{ resp.Hops[i] = &lnrpc.Hop{
ChanId: hop.ChannelID, ChanId: hop.ChannelID,
ChanCapacity: int64(chanCapacity), ChanCapacity: int64(chanCapacity),
@@ -364,6 +377,7 @@ func (r *RouterBackend) MarshallRoute(route *route.Route) (*lnrpc.Route, error)
hop.PubKeyBytes[:], hop.PubKeyBytes[:],
), ),
TlvPayload: !hop.LegacyPayload, TlvPayload: !hop.LegacyPayload,
MppRecord: mpp,
} }
incomingAmt = hop.AmtToForward incomingAmt = hop.AmtToForward
} }
@@ -396,6 +410,11 @@ func (r *RouterBackend) UnmarshallHopByChannelLookup(hop *lnrpc.Hop,
var tlvRecords []tlv.Record var tlvRecords []tlv.Record
mpp, err := UnmarshalMPP(hop.MppRecord)
if err != nil {
return nil, err
}
return &route.Hop{ return &route.Hop{
OutgoingTimeLock: hop.Expiry, OutgoingTimeLock: hop.Expiry,
AmtToForward: lnwire.MilliSatoshi(hop.AmtToForwardMsat), AmtToForward: lnwire.MilliSatoshi(hop.AmtToForwardMsat),
@@ -403,6 +422,7 @@ func (r *RouterBackend) UnmarshallHopByChannelLookup(hop *lnrpc.Hop,
ChannelID: hop.ChanId, ChannelID: hop.ChanId,
TLVRecords: tlvRecords, TLVRecords: tlvRecords,
LegacyPayload: !hop.TlvPayload, LegacyPayload: !hop.TlvPayload,
MPP: mpp,
}, nil }, nil
} }
@@ -420,6 +440,11 @@ func UnmarshallKnownPubkeyHop(hop *lnrpc.Hop) (*route.Hop, error) {
var tlvRecords []tlv.Record var tlvRecords []tlv.Record
mpp, err := UnmarshalMPP(hop.MppRecord)
if err != nil {
return nil, err
}
return &route.Hop{ return &route.Hop{
OutgoingTimeLock: hop.Expiry, OutgoingTimeLock: hop.Expiry,
AmtToForward: lnwire.MilliSatoshi(hop.AmtToForwardMsat), AmtToForward: lnwire.MilliSatoshi(hop.AmtToForwardMsat),
@@ -427,6 +452,7 @@ func UnmarshallKnownPubkeyHop(hop *lnrpc.Hop) (*route.Hop, error) {
ChannelID: hop.ChanId, ChannelID: hop.ChanId,
TLVRecords: tlvRecords, TLVRecords: tlvRecords,
LegacyPayload: !hop.TlvPayload, LegacyPayload: !hop.TlvPayload,
MPP: mpp,
}, nil }, nil
} }
@@ -712,3 +738,45 @@ func ValidateCLTVLimit(val, max uint32) (uint32, error) {
return val, nil return val, nil
} }
} }
// UnmarshalMPP accepts the mpp_total_amt_msat and mpp_payment_addr fields from
// an RPC request and converts into an record.MPP object. An error is returned
// if the payment address is not 0 or 32 bytes. If the total amount and payment
// address are zero-value, the return value will be nil signaling there is no
// MPP record to attach to this hop. Otherwise, a non-nil reocrd will be
// contained combining the provided values.
func UnmarshalMPP(reqMPP *lnrpc.MPPRecord) (*record.MPP, error) {
// If no MPP record was submitted, assume the user wants to send a
// regular payment.
if reqMPP == nil {
return nil, nil
}
reqTotal := reqMPP.TotalAmtMsat
reqAddr := reqMPP.PaymentAddr
switch {
// No MPP fields were provided.
case reqTotal == 0 && len(reqAddr) == 0:
return nil, fmt.Errorf("missing total_msat and payment_addr")
// Total is present, but payment address is missing.
case reqTotal > 0 && len(reqAddr) == 0:
return nil, fmt.Errorf("missing payment_addr")
// Payment address is present, but total is missing.
case reqTotal == 0 && len(reqAddr) > 0:
return nil, fmt.Errorf("missing total_msat")
}
addr, err := lntypes.MakeHash(reqAddr)
if err != nil {
return nil, fmt.Errorf("unable to parse "+
"payment_addr: %v", err)
}
total := lnwire.MilliSatoshi(reqTotal)
return record.NewMPP(total, addr), nil
}
+120
View File
@@ -180,3 +180,123 @@ func (m *mockMissionControl) GetPairHistorySnapshot(fromNode,
return routing.TimedPairResult{} return routing.TimedPairResult{}
} }
type mppOutcome byte
const (
valid mppOutcome = iota
invalid
nompp
)
type unmarshalMPPTest struct {
name string
mpp *lnrpc.MPPRecord
outcome mppOutcome
}
// TestUnmarshalMPP checks both positive and negative cases of UnmarshalMPP to
// assert that an MPP record is only returned when both fields are properly
// specified. It also asserts that zero-values for both inputs is also valid,
// but returns a nil record.
func TestUnmarshalMPP(t *testing.T) {
tests := []unmarshalMPPTest{
{
name: "nil record",
mpp: nil,
outcome: nompp,
},
{
name: "invalid total or addr",
mpp: &lnrpc.MPPRecord{
PaymentAddr: nil,
TotalAmtMsat: 0,
},
outcome: invalid,
},
{
name: "valid total only",
mpp: &lnrpc.MPPRecord{
PaymentAddr: nil,
TotalAmtMsat: 8,
},
outcome: invalid,
},
{
name: "valid addr only",
mpp: &lnrpc.MPPRecord{
PaymentAddr: bytes.Repeat([]byte{0x02}, 32),
TotalAmtMsat: 0,
},
outcome: invalid,
},
{
name: "valid total and invalid addr",
mpp: &lnrpc.MPPRecord{
PaymentAddr: []byte{0x02},
TotalAmtMsat: 8,
},
outcome: invalid,
},
{
name: "valid total and valid addr",
mpp: &lnrpc.MPPRecord{
PaymentAddr: bytes.Repeat([]byte{0x02}, 32),
TotalAmtMsat: 8,
},
outcome: valid,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
testUnmarshalMPP(t, test)
})
}
}
func testUnmarshalMPP(t *testing.T, test unmarshalMPPTest) {
mpp, err := UnmarshalMPP(test.mpp)
switch test.outcome {
// Valid arguments should result in no error, a non-nil MPP record, and
// the fields should be set correctly.
case valid:
if err != nil {
t.Fatalf("unable to parse mpp record: %v", err)
}
if mpp == nil {
t.Fatalf("mpp payload should be non-nil")
}
if int64(mpp.TotalMsat()) != test.mpp.TotalAmtMsat {
t.Fatalf("incorrect total msat")
}
addr := mpp.PaymentAddr()
if !bytes.Equal(addr[:], test.mpp.PaymentAddr) {
t.Fatalf("incorrect payment addr")
}
// Invalid arguments should produce a failure and nil MPP record.
case invalid:
if err == nil {
t.Fatalf("expected failure for invalid mpp")
}
if mpp != nil {
t.Fatalf("mpp payload should be nil for failure")
}
// Arguments that produce no MPP field should return no error and no MPP
// record.
case nompp:
if err != nil {
t.Fatalf("failure for args resulting for no-mpp")
}
if mpp != nil {
t.Fatalf("mpp payload should be nil for no-mpp")
}
default:
t.Fatalf("test case has non-standard outcome")
}
}
+677 -603
View File
File diff suppressed because it is too large. Load diff
+26
View File
@@ -1836,6 +1836,32 @@ message Hop {
TLV format. TLV format.
*/ */
bool tlv_payload = 9 [json_name = "tlv_payload"]; bool tlv_payload = 9 [json_name = "tlv_payload"];
/**
An optional TLV record tha singals the use of an MPP payment. If present,
the receiver will enforce that that the same mpp_record is included in the
final hop payload of all non-zero payments in the HTLC set. If empty, a
regular single-shot payment is or was attempted.
*/
MPPRecord mpp_record = 10 [json_name = "mpp_record"];
}
message MPPRecord {
/**
A unique, random identifier used to authenticate the sender as the intended
payer of a multi-path payment. The payment_addr must be the same for all
subpayments, and match the payment_addr provided in the receiver's invoice.
The same payment_addr must be used on all subpayments.
*/
bytes payment_addr = 11 [json_name = "payment_addr"];
/**
The total amount in milli-satoshis being sent as part of a larger multi-path
payment. The caller is responsible for ensuring subpayments to the same node
and payment_hash sum exactly to total_amt_msat. The same
total_amt_msat must be used on all subpayments.
*/
int64 total_amt_msat = 10 [json_name = "total_amt_msat"];
} }
/** /**
+19
View File
@@ -2530,6 +2530,10 @@
"type": "boolean", "type": "boolean",
"format": "boolean", "format": "boolean",
"description": "* \nIf set to true, then this hop will be encoded using the new variable length\nTLV format." "description": "* \nIf set to true, then this hop will be encoded using the new variable length\nTLV format."
},
"mpp_record": {
"$ref": "#/definitions/lnrpcMPPRecord",
"description": "*\nAn optional TLV record tha singals the use of an MPP payment. If present,\nthe receiver will enforce that that the same mpp_record is included in the\nfinal hop payload of all non-zero payments in the HTLC set. If empty, a\nregular single-shot payment is or was attempted."
} }
} }
}, },
@@ -2873,6 +2877,21 @@
} }
} }
}, },
"lnrpcMPPRecord": {
"type": "object",
"properties": {
"payment_addr": {
"type": "string",
"format": "byte",
"description": "*\nA unique, random identifier used to authenticate the sender as the intended\npayer of a multi-path payment. The payment_addr must be the same for all\nsubpayments, and match the payment_addr provided in the receiver's invoice.\nThe same payment_addr must be used on all subpayments."
},
"total_amt_msat": {
"type": "string",
"format": "int64",
"description": "*\nThe total amount in milli-satoshis being sent as part of a larger multi-path\npayment. The caller is responsible for ensuring subpayments to the same node\nand payment_hash sum exactly to total_amt_msat. The same\ntotal_amt_msat must be used on all subpayments."
}
}
},
"lnrpcMacaroonPermission": { "lnrpcMacaroonPermission": {
"type": "object", "type": "object",
"properties": { "properties": {
+297 -76
View File
@@ -106,7 +106,6 @@ func (h *harnessTest) Fatalf(format string, a ...interface{}) {
// RunTestCase executes a harness test case. Any errors or panics will be // RunTestCase executes a harness test case. Any errors or panics will be
// represented as fatal. // represented as fatal.
func (h *harnessTest) RunTestCase(testCase *testCase) { func (h *harnessTest) RunTestCase(testCase *testCase) {
h.testCase = testCase h.testCase = testCase
defer func() { defer func() {
h.testCase = nil h.testCase = nil
@@ -4406,43 +4405,135 @@ func testMultiHopPayments(net *lntest.NetworkHarness, t *harnessTest) {
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false) closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
} }
// testSingleHopSendToRoute tests that payments are properly processed type singleHopSendToRouteCase struct {
// through a provided route with a single hop. We'll create the name string
// following network topology:
// Alice --100k--> Bob // streaming tests streaming SendToRoute if true, otherwise tests
// We'll query the daemon for routes from Alice to Bob and then // synchronous SenToRoute.
// send payments through the route. streaming bool
// routerrpc submits the request to the routerrpc subserver if true,
// otherwise submits to the main rpc server.
routerrpc bool
// mpp sets the MPP fields on the request if true, otherwise submits a
// regular payment.
mpp bool
}
var singleHopSendToRouteCases = []singleHopSendToRouteCase{
{
name: "regular main sync",
},
{
name: "regular main stream",
streaming: true,
},
{
name: "regular routerrpc sync",
routerrpc: true,
},
{
name: "mpp main sync",
mpp: true,
},
{
name: "mpp main stream",
streaming: true,
mpp: true,
},
{
name: "mpp routerrpc sync",
routerrpc: true,
mpp: true,
},
}
// testSingleHopSendToRoute tests that payments are properly processed through a
// provided route with a single hop. We'll create the following network
// topology:
// Carol --100k--> Dave
// We'll query the daemon for routes from Carol to Dave and then send payments
// by feeding the route back into the various SendToRoute RPC methods. Here we
// test all three SendToRoute endpoints, forcing each to perform both a regular
// payment and an MPP payment.
func testSingleHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest) { func testSingleHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest) {
ctxb := context.Background() for _, test := range singleHopSendToRouteCases {
test := test
t.t.Run(test.name, func(t1 *testing.T) {
ht := newHarnessTest(t1, t.lndHarness)
ht.RunTestCase(&testCase{
name: test.name,
test: func(_ *lntest.NetworkHarness, tt *harnessTest) {
testSingleHopSendToRouteCase(net, tt, test)
},
})
})
}
}
func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest,
test singleHopSendToRouteCase) {
const chanAmt = btcutil.Amount(100000) const chanAmt = btcutil.Amount(100000)
const paymentAmtSat = 1000
const numPayments = 5
const amountPaid = int64(numPayments * paymentAmtSat)
ctxb := context.Background()
var networkChans []*lnrpc.ChannelPoint var networkChans []*lnrpc.ChannelPoint
// Open a channel with 100k satoshis between Alice and Bob with Alice // Create Carol and Dave, then establish a channel between them. Carol
// is the sole funder of the channel with 100k satoshis. The network
// topology should look like:
// Carol -> 100k -> Dave
carol, err := net.NewNode("Carol", nil)
if err != nil {
t.Fatalf("unable to create new nodes: %v", err)
}
defer shutdownAndAssert(net, t, carol)
dave, err := net.NewNode("Dave", nil)
if err != nil {
t.Fatalf("unable to create new nodes: %v", err)
}
defer shutdownAndAssert(net, t, dave)
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
if err := net.ConnectNodes(ctxt, carol, dave); err != nil {
t.Fatalf("unable to connect carol to dave: %v", err)
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
err = net.SendCoins(ctxt, btcutil.SatoshiPerBitcoin, carol)
if err != nil {
t.Fatalf("unable to send coins to carol: %v", err)
}
// Open a channel with 100k satoshis between Carol and Dave with Carol
// being the sole funder of the channel. // being the sole funder of the channel.
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout) ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
chanPointAlice := openChannelAndAssert( chanPointCarol := openChannelAndAssert(
ctxt, t, net, net.Alice, net.Bob, ctxt, t, net, carol, dave,
lntest.OpenChannelParams{ lntest.OpenChannelParams{
Amt: chanAmt, Amt: chanAmt,
}, },
) )
networkChans = append(networkChans, chanPointAlice) networkChans = append(networkChans, chanPointCarol)
aliceChanTXID, err := lnd.GetChanPointFundingTxid(chanPointAlice) carolChanTXID, err := lnd.GetChanPointFundingTxid(chanPointCarol)
if err != nil { if err != nil {
t.Fatalf("unable to get txid: %v", err) t.Fatalf("unable to get txid: %v", err)
} }
aliceFundPoint := wire.OutPoint{ carolFundPoint := wire.OutPoint{
Hash: *aliceChanTXID, Hash: *carolChanTXID,
Index: chanPointAlice.OutputIndex, Index: chanPointCarol.OutputIndex,
} }
// Wait for all nodes to have seen all channels. // Wait for all nodes to have seen all channels.
nodes := []*lntest.HarnessNode{net.Alice, net.Bob} nodes := []*lntest.HarnessNode{carol, dave}
nodeNames := []string{"Alice", "Bob"}
for _, chanPoint := range networkChans { for _, chanPoint := range networkChans {
for i, node := range nodes { for _, node := range nodes {
txid, err := lnd.GetChanPointFundingTxid(chanPoint) txid, err := lnd.GetChanPointFundingTxid(chanPoint)
if err != nil { if err != nil {
t.Fatalf("unable to get txid: %v", err) t.Fatalf("unable to get txid: %v", err)
@@ -4456,57 +4547,97 @@ func testSingleHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest) {
err = node.WaitForNetworkChannelOpen(ctxt, chanPoint) err = node.WaitForNetworkChannelOpen(ctxt, chanPoint)
if err != nil { if err != nil {
t.Fatalf("%s(%d): timeout waiting for "+ t.Fatalf("%s(%d): timeout waiting for "+
"channel(%s) open: %v", nodeNames[i], "channel(%s) open: %v", node.Name(),
node.NodeID, point, err) node.NodeID, point, err)
} }
} }
} }
// Query for routes to pay from Alice to Bob. // Create invoices for Dave, which expect a payment from Carol.
// We set FinalCltvDelta to 40 since by default QueryRoutes returns
// the last hop with a final cltv delta of 9 where as the default in
// htlcswitch is 40.
const paymentAmt = 1000
routesReq := &lnrpc.QueryRoutesRequest{
PubKey: net.Bob.PubKeyStr,
Amt: paymentAmt,
FinalCltvDelta: lnd.DefaultBitcoinTimeLockDelta,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
routes, err := net.Alice.QueryRoutes(ctxt, routesReq)
if err != nil {
t.Fatalf("unable to get route: %v", err)
}
// Create 5 invoices for Bob, which expect a payment from Alice for 1k
// satoshis with a different preimage each time.
const numPayments = 5
_, rHashes, _, err := createPayReqs( _, rHashes, _, err := createPayReqs(
net.Bob, paymentAmt, numPayments, dave, paymentAmtSat, numPayments,
) )
if err != nil { if err != nil {
t.Fatalf("unable to create pay reqs: %v", err) t.Fatalf("unable to create pay reqs: %v", err)
} }
// We'll wait for all parties to recognize the new channels within the // Query for routes to pay from Carol to Dave.
// network. // We set FinalCltvDelta to 40 since by default QueryRoutes returns
// the last hop with a final cltv delta of 9 where as the default in
// htlcswitch is 40.
routesReq := &lnrpc.QueryRoutesRequest{
PubKey: dave.PubKeyStr,
Amt: paymentAmtSat,
FinalCltvDelta: lnd.DefaultBitcoinTimeLockDelta,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
err = net.Bob.WaitForNetworkChannelOpen(ctxt, chanPointAlice) routes, err := carol.QueryRoutes(ctxt, routesReq)
if err != nil { if err != nil {
t.Fatalf("alice didn't advertise her channel in time: %v", err) t.Fatalf("unable to get route from %s: %v",
carol.Name(), err)
} }
time.Sleep(time.Millisecond * 50) // There should only be one route to try, so take the first item.
r := routes.Routes[0]
// Using Alice as the source, pay to the 5 invoices from Carol created // Construct a closure that will set MPP fields on the route, which
// above. // allows us to test MPP payments.
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout) setMPPFields := func(i int) {
alicePayStream, err := net.Alice.SendToRoute(ctxt) addr := [32]byte{byte(i)}
if err != nil {
t.Fatalf("unable to create payment stream for alice: %v", err) hop := r.Hops[len(r.Hops)-1]
hop.TlvPayload = true
hop.MppRecord = &lnrpc.MPPRecord{
PaymentAddr: addr[:],
TotalAmtMsat: paymentAmtSat * 1000,
}
}
// Construct closures for each of the payment types covered:
// - main rpc server sync
// - main rpc server streaming
// - routerrpc server sync
sendToRouteSync := func() {
for i, rHash := range rHashes {
// Populate the MPP fields for the final hop if we are
// testing MPP payments.
if test.mpp {
setMPPFields(i)
}
sendReq := &lnrpc.SendToRouteRequest{
PaymentHash: rHash,
Route: r,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
resp, err := carol.SendToRouteSync(
ctxt, sendReq,
)
if err != nil {
t.Fatalf("unable to send to route for "+
"%s: %v", carol.Name(), err)
}
if resp.PaymentError != "" {
t.Fatalf("received payment error from %s: %v",
carol.Name(), resp.PaymentError)
}
}
}
sendToRouteStream := func() {
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
alicePayStream, err := carol.SendToRoute(ctxt)
if err != nil {
t.Fatalf("unable to create payment stream for "+
"carol: %v", err)
}
for i, rHash := range rHashes {
// Populate the MPP fields for the final hop if we are
// testing MPP payments.
if test.mpp {
setMPPFields(i)
} }
for _, rHash := range rHashes {
sendReq := &lnrpc.SendToRouteRequest{ sendReq := &lnrpc.SendToRouteRequest{
PaymentHash: rHash, PaymentHash: rHash,
Route: routes.Routes[0], Route: routes.Routes[0],
@@ -4516,51 +4647,141 @@ func testSingleHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest) {
if err != nil { if err != nil {
t.Fatalf("unable to send payment: %v", err) t.Fatalf("unable to send payment: %v", err)
} }
}
for range rHashes {
resp, err := alicePayStream.Recv() resp, err := alicePayStream.Recv()
if err != nil { if err != nil {
t.Fatalf("unable to send payment: %v", err) t.Fatalf("unable to send payment: %v", err)
} }
if resp.PaymentError != "" { if resp.PaymentError != "" {
t.Fatalf("received payment error: %v", resp.PaymentError) t.Fatalf("received payment error: %v",
resp.PaymentError)
} }
} }
}
sendToRouteRouterRPC := func() {
for i, rHash := range rHashes {
// Populate the MPP fields for the final hop if we are
// testing MPP payments.
if test.mpp {
setMPPFields(i)
}
req := &lnrpc.ListPaymentsRequest{} sendReq := &routerrpc.SendToRouteRequest{
ctxt, _ = context.WithTimeout(ctxt, defaultTimeout) PaymentHash: rHash,
paymentsResp, err := net.Alice.ListPayments(ctxt, req) Route: r,
}
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
resp, err := carol.RouterClient.SendToRoute(
ctxt, sendReq,
)
if err != nil { if err != nil {
t.Fatalf("error when obtaining Alice payments: %v", err) t.Fatalf("unable to send to route for "+
"%s: %v", carol.Name(), err)
}
if resp.Failure != nil {
t.Fatalf("received payment error from %s: %v",
carol.Name(), resp.Failure)
}
} }
if len(paymentsResp.Payments) != 5 {
t.Fatalf("incorrect number of payments, got %v, want %v",
len(paymentsResp.Payments), 5)
} }
// Verify that the ListPayments displays the payment without an invoice // Using Carol as the node as the source, send the payments
// since the payment was completed with SendToRoute. // synchronously via the the routerrpc's SendToRoute, or via the main RPC
for _, p := range paymentsResp.Payments { // server's SendToRoute streaming or sync calls.
switch {
case !test.routerrpc && test.streaming:
sendToRouteStream()
case !test.routerrpc && !test.streaming:
sendToRouteSync()
case test.routerrpc && !test.streaming:
sendToRouteRouterRPC()
default:
t.Fatalf("routerrpc does not support streaming send_to_route")
}
// Verify that the payment's from Carol's PoV have the correct payment
// hash and amount.
ctxt, _ = context.WithTimeout(ctxt, defaultTimeout)
paymentsResp, err := carol.ListPayments(
ctxt, &lnrpc.ListPaymentsRequest{},
)
if err != nil {
t.Fatalf("error when obtaining %s payments: %v",
carol.Name(), err)
}
if len(paymentsResp.Payments) != numPayments {
t.Fatalf("incorrect number of payments, got %v, want %v",
len(paymentsResp.Payments), numPayments)
}
for i, p := range paymentsResp.Payments {
// Assert that the payment hashes for each payment match up.
rHashHex := hex.EncodeToString(rHashes[i])
if p.PaymentHash != rHashHex {
t.Fatalf("incorrect payment hash for payment %d, "+
"want: %s got: %s",
i, rHashHex, p.PaymentHash)
}
// Assert that each payment has no invoice since the payment was
// completed using SendToRoute.
if p.PaymentRequest != "" { if p.PaymentRequest != "" {
t.Fatalf("incorrect payreq, want: \"\", got: %v", t.Fatalf("incorrect payment request for payment: %d, "+
p.PaymentRequest) "want: \"\", got: %s",
i, p.PaymentRequest)
}
// Assert the payment ammount is correct.
if p.ValueSat != paymentAmtSat {
t.Fatalf("incorrect payment amt for payment %d, "+
"want: %d, got: %d",
i, paymentAmtSat, p.ValueSat)
}
}
// Verify that the invoices's from Dave's PoV have the correct payment
// hash and amount.
ctxt, _ = context.WithTimeout(ctxt, defaultTimeout)
invoicesResp, err := dave.ListInvoices(
ctxt, &lnrpc.ListInvoiceRequest{},
)
if err != nil {
t.Fatalf("error when obtaining %s payments: %v",
dave.Name(), err)
}
if len(invoicesResp.Invoices) != numPayments {
t.Fatalf("incorrect number of invoices, got %v, want %v",
len(invoicesResp.Invoices), numPayments)
}
for i, inv := range invoicesResp.Invoices {
// Assert that the payment hashes match up.
if !bytes.Equal(inv.RHash, rHashes[i]) {
t.Fatalf("incorrect payment hash for invoice %d, "+
"want: %x got: %x",
i, rHashes[i], inv.RHash)
}
// Assert that the amount paid to the invoice is correct.
if inv.AmtPaidSat != paymentAmtSat {
t.Fatalf("incorrect payment amt for invoice %d, "+
"want: %d, got %d",
i, paymentAmtSat, inv.AmtPaidSat)
} }
} }
// At this point all the channels within our proto network should be // At this point all the channels within our proto network should be
// shifted by 5k satoshis in the direction of Bob, the sink within the // shifted by 5k satoshis in the direction of Dave, the sink within the
// payment flow generated above. The order of asserts corresponds to // payment flow generated above. The order of asserts corresponds to
// increasing of time is needed to embed the HTLC in commitment // increasing of time is needed to embed the HTLC in commitment
// transaction, in channel Alice->Bob, order is Bob and then Alice. // transaction, in channel Carol->Dave, order is Dave and then Carol.
const amountPaid = int64(5000) assertAmountPaid(t, "Carol(local) => Dave(remote)", dave,
assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Bob, carolFundPoint, int64(0), amountPaid)
aliceFundPoint, int64(0), amountPaid) assertAmountPaid(t, "Carol(local) => Dave(remote)", carol,
assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Alice, carolFundPoint, amountPaid, int64(0))
aliceFundPoint, amountPaid, int64(0))
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout) ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false) closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
} }
// testMultiHopSendToRoute tests that payments are properly processed // testMultiHopSendToRoute tests that payments are properly processed
+98
View File
@@ -0,0 +1,98 @@
package record
import (
"io"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
)
// MPPOnionType is the type used in the onion to reference the MPP fields:
// total_amt and payment_addr.
const MPPOnionType tlv.Type = 8
// MPP is a record that encodes the fields necessary for multi-path payments.
type MPP struct {
// paymentAddr is a random, receiver-generated value used to avoid
// collisions with concurrent payers.
paymentAddr [32]byte
// totalMsat is the total value of the payment, potentially spread
// across more than one HTLC.
totalMsat lnwire.MilliSatoshi
}
// NewMPP generates a new MPP record with the given total and payment address.
func NewMPP(total lnwire.MilliSatoshi, addr [32]byte) *MPP {
return &MPP{
paymentAddr: addr,
totalMsat: total,
}
}
// PaymentAddr returns the payment address contained in the MPP record.
func (r *MPP) PaymentAddr() [32]byte {
return r.paymentAddr
}
// TotalMsat returns the total value of an MPP payment in msats.
func (r *MPP) TotalMsat() lnwire.MilliSatoshi {
return r.totalMsat
}
// MPPEncoder writes the MPP record to the provided io.Writer.
func MPPEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
if v, ok := val.(*MPP); ok {
err := tlv.EBytes32(w, &v.paymentAddr, buf)
if err != nil {
return err
}
return tlv.ETUint64T(w, uint64(v.totalMsat), buf)
}
return tlv.NewTypeForEncodingErr(val, "MPP")
}
const (
// minMPPLength is the minimum length of a serialized MPP TLV record,
// which occurs when the truncated encoding of total_amt_msat takes 0
// bytes, leaving only the payment_addr.
minMPPLength = 32
// maxMPPLength is the maximum length of a serialized MPP TLV record,
// which occurs when the truncated encoding of total_amt_msat takes 8
// bytes.
maxMPPLength = 40
)
// MPPDecoder reads the MPP record to the provided io.Reader.
func MPPDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
if v, ok := val.(*MPP); ok && minMPPLength <= l && l <= maxMPPLength {
if err := tlv.DBytes32(r, &v.paymentAddr, buf, 32); err != nil {
return err
}
var total uint64
if err := tlv.DTUint64(r, &total, buf, l-32); err != nil {
return err
}
v.totalMsat = lnwire.MilliSatoshi(total)
return nil
}
return tlv.NewTypeForDecodingErr(val, "MPP", l, maxMPPLength)
}
// Record returns a tlv.Record that can be used to encode or decode this record.
func (r *MPP) Record() tlv.Record {
// Fixed-size, 32 byte payment address followed by truncated 64-bit
// total msat.
size := func() uint64 {
return 32 + tlv.SizeTUint64(uint64(r.totalMsat))
}
return tlv.MakeDynamicRecord(
MPPOnionType, r, size, MPPEncoder, MPPDecoder,
)
}
+73
View File
@@ -0,0 +1,73 @@
package record_test
import (
"bytes"
"testing"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/tlv"
)
type recordEncDecTest struct {
name string
encRecord func() tlv.RecordProducer
decRecord func() tlv.RecordProducer
assert func(*testing.T, interface{})
}
var (
testTotal = lnwire.MilliSatoshi(45)
testAddr = [32]byte{0x01, 0x02}
)
var recordEncDecTests = []recordEncDecTest{
{
name: "mpp",
encRecord: func() tlv.RecordProducer {
return record.NewMPP(testTotal, testAddr)
},
decRecord: func() tlv.RecordProducer {
return new(record.MPP)
},
assert: func(t *testing.T, r interface{}) {
mpp := r.(*record.MPP)
if mpp.TotalMsat() != testTotal {
t.Fatal("incorrect total msat")
}
if mpp.PaymentAddr() != testAddr {
t.Fatal("incorrect payment addr")
}
},
},
}
// TestRecordEncodeDecode is a generic test framework for custom TLV records. It
// asserts that records can encode and decode themselves, and that the value of
// the original record matches the decoded record.
func TestRecordEncodeDecode(t *testing.T) {
for _, test := range recordEncDecTests {
test := test
t.Run(test.name, func(t *testing.T) {
r := test.encRecord()
r2 := test.decRecord()
encStream := tlv.MustNewStream(r.Record())
decStream := tlv.MustNewStream(r2.Record())
test.assert(t, r)
var b bytes.Buffer
err := encStream.Encode(&b)
if err != nil {
t.Fatalf("unable to encode record: %v", err)
}
err = decStream.Decode(bytes.NewReader(b.Bytes()))
if err != nil {
t.Fatalf("unable to decode record: %v", err)
}
test.assert(t, r2)
})
}
}
+27 -3
View File
@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"io" "io"
"strconv" "strconv"
@@ -19,9 +20,17 @@ import (
// VertexSize is the size of the array to store a vertex. // VertexSize is the size of the array to store a vertex.
const VertexSize = 33 const VertexSize = 33
// ErrNoRouteHopsProvided is returned when a caller attempts to construct a new var (
// sphinx packet, but provides an empty set of hops for each route. // ErrNoRouteHopsProvided is returned when a caller attempts to
var ErrNoRouteHopsProvided = fmt.Errorf("empty route hops provided") // construct a new sphinx packet, but provides an empty set of hops for
// each route.
ErrNoRouteHopsProvided = fmt.Errorf("empty route hops provided")
// ErrIntermediateMPPHop is returned when a hop tries to deliver an MPP
// record to an intermediate hop, only final hops can receive MPP
// records.
ErrIntermediateMPPHop = errors.New("cannot send MPP to intermediate")
)
// Vertex is a simple alias for the serialization of a compressed Bitcoin // Vertex is a simple alias for the serialization of a compressed Bitcoin
// public key. // public key.
@@ -94,6 +103,10 @@ type Hop struct {
// carries as a fee will be subtracted by the hop. // carries as a fee will be subtracted by the hop.
AmtToForward lnwire.MilliSatoshi AmtToForward lnwire.MilliSatoshi
// MPP encapsulates the data required for option_mpp. This field should
// only be set for the final hop.
MPP *record.MPP
// TLVRecords if non-nil are a set of additional TLV records that // TLVRecords if non-nil are a set of additional TLV records that
// should be included in the forwarding instructions for this node. // should be included in the forwarding instructions for this node.
TLVRecords []tlv.Record TLVRecords []tlv.Record
@@ -140,6 +153,17 @@ func (h *Hop) PackHopPayload(w io.Writer, nextChanID uint64) error {
) )
} }
// If an MPP record is destined for this hop, ensure that we only ever
// attach it to the final hop. Otherwise the route was constructed
// incorrectly.
if h.MPP != nil {
if nextChanID == 0 {
records = append(records, h.MPP.Record())
} else {
return ErrIntermediateMPPHop
}
}
// Append any custom types destined for this hop. // Append any custom types destined for this hop.
records = append(records, h.TLVRecords...) records = append(records, h.TLVRecords...)
+37
View File
@@ -1,9 +1,11 @@
package route package route
import ( import (
"bytes"
"testing" "testing"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
) )
// TestRouteTotalFees checks that a route reports the expected total fee. // TestRouteTotalFees checks that a route reports the expected total fee.
@@ -56,3 +58,38 @@ func TestRouteTotalFees(t *testing.T) {
} }
} }
var (
testAmt = lnwire.MilliSatoshi(1000)
testAddr = [32]byte{0x01, 0x02}
)
// TestMPPHop asserts that a Hop will encode a non-nil to final nodes, and fail
// when trying to send to intermediaries.
func TestMPPHop(t *testing.T) {
t.Parallel()
hop := Hop{
ChannelID: 1,
OutgoingTimeLock: 44,
AmtToForward: testAmt,
LegacyPayload: false,
MPP: record.NewMPP(testAmt, testAddr),
}
// Encoding an MPP record to an intermediate hop should result in a
// failure.
var b bytes.Buffer
err := hop.PackHopPayload(&b, 2)
if err != ErrIntermediateMPPHop {
t.Fatalf("expected err: %v, got: %v",
ErrIntermediateMPPHop, err)
}
// Encoding an MPP record to a final hop should be successful.
b.Reset()
err = hop.PackHopPayload(&b, 0)
if err != nil {
t.Fatalf("expected err: %v, got: %v", nil, err)
}
}
+16
View File
@@ -43,6 +43,14 @@ func SizeVarBytes(e *[]byte) SizeFunc {
} }
} }
// RecorderProducer is an interface for objects that can produce a Record object
// capable of encoding and/or decoding the RecordProducer as a Record.
type RecordProducer interface {
// Record returns a Record that can be used to encode or decode the
// backing object.
Record() Record
}
// Record holds the required information to encode or decode a TLV record. // Record holds the required information to encode or decode a TLV record.
type Record struct { type Record struct {
value interface{} value interface{}
@@ -77,6 +85,14 @@ func (f *Record) Encode(w io.Writer) error {
return f.encoder(w, f.value, &b) return f.encoder(w, f.value, &b)
} }
// Decode read in the TLV record from the passed reader. This is useful when a
// caller wants decode a *single* TLV record, outside the context of the Stream
// struct.
func (f *Record) Decode(r io.Reader, l uint64) error {
var b [8]byte
return f.decoder(r, f.value, &b, l)
}
// MakePrimitiveRecord creates a record for common types. // MakePrimitiveRecord creates a record for common types.
func MakePrimitiveRecord(typ Type, val interface{}) Record { func MakePrimitiveRecord(typ Type, val interface{}) Record {
var ( var (
+27
View File
@@ -40,6 +40,15 @@ func ETUint16(w io.Writer, val interface{}, buf *[8]byte) error {
return NewTypeForEncodingErr(val, "uint16") return NewTypeForEncodingErr(val, "uint16")
} }
// ETUint16T is an Encoder for truncated uint16 values, where leading zeros will
// be omitted. An error is returned if val is not a *uint16.
func ETUint16T(w io.Writer, val uint16, buf *[8]byte) error {
binary.BigEndian.PutUint16(buf[:2], val)
numZeros := numLeadingZeroBytes16(val)
_, err := w.Write(buf[numZeros:2])
return err
}
// DTUint16 is an Decoder for truncated uint16 values, where leading zeros will // DTUint16 is an Decoder for truncated uint16 values, where leading zeros will
// be resurrected. An error is returned if val is not a *uint16. // be resurrected. An error is returned if val is not a *uint16.
func DTUint16(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { func DTUint16(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
@@ -92,6 +101,15 @@ func ETUint32(w io.Writer, val interface{}, buf *[8]byte) error {
return NewTypeForEncodingErr(val, "uint32") return NewTypeForEncodingErr(val, "uint32")
} }
// ETUint32T is an Encoder for truncated uint32 values, where leading zeros will
// be omitted. An error is returned if val is not a *uint32.
func ETUint32T(w io.Writer, val uint32, buf *[8]byte) error {
binary.BigEndian.PutUint32(buf[:4], val)
numZeros := numLeadingZeroBytes32(val)
_, err := w.Write(buf[numZeros:4])
return err
}
// DTUint32 is an Decoder for truncated uint32 values, where leading zeros will // DTUint32 is an Decoder for truncated uint32 values, where leading zeros will
// be resurrected. An error is returned if val is not a *uint32. // be resurrected. An error is returned if val is not a *uint32.
func DTUint32(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { func DTUint32(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
@@ -154,6 +172,15 @@ func ETUint64(w io.Writer, val interface{}, buf *[8]byte) error {
return NewTypeForEncodingErr(val, "uint64") return NewTypeForEncodingErr(val, "uint64")
} }
// ETUint64T is an Encoder for truncated uint64 values, where leading zeros will
// be omitted. An error is returned if val is not a *uint64.
func ETUint64T(w io.Writer, val uint64, buf *[8]byte) error {
binary.BigEndian.PutUint64(buf[:], val)
numZeros := numLeadingZeroBytes64(val)
_, err := w.Write(buf[numZeros:])
return err
}
// DTUint64 is an Decoder for truncated uint64 values, where leading zeros will // DTUint64 is an Decoder for truncated uint64 values, where leading zeros will
// be resurrected. An error is returned if val is not a *uint64. // be resurrected. An error is returned if val is not a *uint64.
func DTUint64(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { func DTUint64(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
+48
View File
@@ -60,6 +60,8 @@ func TestSizeTUint16(t *testing.T) {
func TestTUint16(t *testing.T) { func TestTUint16(t *testing.T) {
var buf [8]byte var buf [8]byte
for _, test := range tuint16Tests { for _, test := range tuint16Tests {
test := test
if len(test.bytes) != int(test.size) { if len(test.bytes) != int(test.size) {
t.Fatalf("invalid test case, "+ t.Fatalf("invalid test case, "+
"len(bytes)[%d] != size[%d]", "len(bytes)[%d] != size[%d]",
@@ -68,6 +70,7 @@ func TestTUint16(t *testing.T) {
name := fmt.Sprintf("0x%x", test.value) name := fmt.Sprintf("0x%x", test.value)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
// Test generic encoder.
var b bytes.Buffer var b bytes.Buffer
err := tlv.ETUint16(&b, &test.value, &buf) err := tlv.ETUint16(&b, &test.value, &buf)
if err != nil { if err != nil {
@@ -80,6 +83,19 @@ func TestTUint16(t *testing.T) {
test.bytes, b.Bytes()) test.bytes, b.Bytes())
} }
// Test non-generic encoder.
var b2 bytes.Buffer
err = tlv.ETUint16T(&b2, test.value, &buf)
if err != nil {
t.Fatalf("unable to encode tuint16: %v", err)
}
if !bytes.Equal(b2.Bytes(), test.bytes) {
t.Fatalf("encoding mismatch, "+
"expected: %x, got: %x",
test.bytes, b2.Bytes())
}
var value uint16 var value uint16
r := bytes.NewReader(b.Bytes()) r := bytes.NewReader(b.Bytes())
err = tlv.DTUint16(r, &value, &buf, test.size) err = tlv.DTUint16(r, &value, &buf, test.size)
@@ -168,6 +184,8 @@ func TestSizeTUint32(t *testing.T) {
func TestTUint32(t *testing.T) { func TestTUint32(t *testing.T) {
var buf [8]byte var buf [8]byte
for _, test := range tuint32Tests { for _, test := range tuint32Tests {
test := test
if len(test.bytes) != int(test.size) { if len(test.bytes) != int(test.size) {
t.Fatalf("invalid test case, "+ t.Fatalf("invalid test case, "+
"len(bytes)[%d] != size[%d]", "len(bytes)[%d] != size[%d]",
@@ -176,6 +194,7 @@ func TestTUint32(t *testing.T) {
name := fmt.Sprintf("0x%x", test.value) name := fmt.Sprintf("0x%x", test.value)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
// Test generic encoder.
var b bytes.Buffer var b bytes.Buffer
err := tlv.ETUint32(&b, &test.value, &buf) err := tlv.ETUint32(&b, &test.value, &buf)
if err != nil { if err != nil {
@@ -188,6 +207,19 @@ func TestTUint32(t *testing.T) {
test.bytes, b.Bytes()) test.bytes, b.Bytes())
} }
// Test non-generic encoder.
var b2 bytes.Buffer
err = tlv.ETUint32T(&b2, test.value, &buf)
if err != nil {
t.Fatalf("unable to encode tuint32: %v", err)
}
if !bytes.Equal(b2.Bytes(), test.bytes) {
t.Fatalf("encoding mismatch, "+
"expected: %x, got: %x",
test.bytes, b2.Bytes())
}
var value uint32 var value uint32
r := bytes.NewReader(b.Bytes()) r := bytes.NewReader(b.Bytes())
err = tlv.DTUint32(r, &value, &buf, test.size) err = tlv.DTUint32(r, &value, &buf, test.size)
@@ -322,6 +354,8 @@ func TestSizeTUint64(t *testing.T) {
func TestTUint64(t *testing.T) { func TestTUint64(t *testing.T) {
var buf [8]byte var buf [8]byte
for _, test := range tuint64Tests { for _, test := range tuint64Tests {
test := test
if len(test.bytes) != int(test.size) { if len(test.bytes) != int(test.size) {
t.Fatalf("invalid test case, "+ t.Fatalf("invalid test case, "+
"len(bytes)[%d] != size[%d]", "len(bytes)[%d] != size[%d]",
@@ -330,6 +364,7 @@ func TestTUint64(t *testing.T) {
name := fmt.Sprintf("0x%x", test.value) name := fmt.Sprintf("0x%x", test.value)
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
// Test generic encoder.
var b bytes.Buffer var b bytes.Buffer
err := tlv.ETUint64(&b, &test.value, &buf) err := tlv.ETUint64(&b, &test.value, &buf)
if err != nil { if err != nil {
@@ -342,6 +377,19 @@ func TestTUint64(t *testing.T) {
test.bytes, b.Bytes()) test.bytes, b.Bytes())
} }
// Test non-generic encoder.
var b2 bytes.Buffer
err = tlv.ETUint64T(&b2, test.value, &buf)
if err != nil {
t.Fatalf("unable to encode tuint64: %v", err)
}
if !bytes.Equal(b2.Bytes(), test.bytes) {
t.Fatalf("encoding mismatch, "+
"expected: %x, got: %x",
test.bytes, b2.Bytes())
}
var value uint64 var value uint64
r := bytes.NewReader(b.Bytes()) r := bytes.NewReader(b.Bytes())
err = tlv.DTUint64(r, &value, &buf, test.size) err = tlv.DTUint64(r, &value, &buf, test.size)