mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-06 02:12:18 +01:00
This commit adds the encrypted_data, blinding_point and total_amt_msat tlvs to the known set of even tlvs for the onion payload. These TLVs are added in two places (the onion payload and hop struct) because lnd uses the same set of TLV types for both structs (and they inherently represent the same thing). Note: in some places, unit tests intentionally mimic the style of older tests, so as to be more consistently readable.
440 lines
11 KiB
Go
440 lines
11 KiB
Go
package hop_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/lightningnetwork/lnd/htlcswitch/hop"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/record"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var (
|
|
//nolint:lll
|
|
testPrivKeyBytes, _ = hex.DecodeString("e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734")
|
|
_, testPubKey = btcec.PrivKeyFromBytes(testPrivKeyBytes)
|
|
)
|
|
|
|
const testUnknownRequiredType = 0x80
|
|
|
|
type decodePayloadTest struct {
|
|
name string
|
|
payload []byte
|
|
expErr error
|
|
expCustomRecords map[uint64][]byte
|
|
shouldHaveMPP bool
|
|
shouldHaveAMP bool
|
|
shouldHaveEncData bool
|
|
shouldHaveBlinding bool
|
|
shouldHaveMetadata bool
|
|
shouldHaveTotalAmt bool
|
|
}
|
|
|
|
var decodePayloadTests = []decodePayloadTest{
|
|
{
|
|
name: "final hop valid",
|
|
payload: []byte{0x02, 0x00, 0x04, 0x00},
|
|
},
|
|
{
|
|
name: "intermediate hop valid",
|
|
payload: []byte{0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x01, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
},
|
|
},
|
|
{
|
|
name: "final hop no amount",
|
|
payload: []byte{0x04, 0x00},
|
|
expErr: hop.ErrInvalidPayload{
|
|
Type: record.AmtOnionType,
|
|
Violation: hop.OmittedViolation,
|
|
FinalHop: true,
|
|
},
|
|
},
|
|
{
|
|
name: "intermediate hop no amount",
|
|
payload: []byte{0x04, 0x00, 0x06, 0x08, 0x01, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
},
|
|
expErr: hop.ErrInvalidPayload{
|
|
Type: record.AmtOnionType,
|
|
Violation: hop.OmittedViolation,
|
|
FinalHop: false,
|
|
},
|
|
},
|
|
{
|
|
name: "final hop no expiry",
|
|
payload: []byte{0x02, 0x00},
|
|
expErr: hop.ErrInvalidPayload{
|
|
Type: record.LockTimeOnionType,
|
|
Violation: hop.OmittedViolation,
|
|
FinalHop: true,
|
|
},
|
|
},
|
|
{
|
|
name: "intermediate hop no expiry",
|
|
payload: []byte{0x02, 0x00, 0x06, 0x08, 0x01, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
},
|
|
expErr: hop.ErrInvalidPayload{
|
|
Type: record.LockTimeOnionType,
|
|
Violation: hop.OmittedViolation,
|
|
FinalHop: false,
|
|
},
|
|
},
|
|
{
|
|
name: "final hop next sid present",
|
|
payload: []byte{0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
},
|
|
expErr: hop.ErrInvalidPayload{
|
|
Type: record.NextHopOnionType,
|
|
Violation: hop.IncludedViolation,
|
|
FinalHop: true,
|
|
},
|
|
},
|
|
{
|
|
name: "required type after omitted hop id",
|
|
payload: []byte{
|
|
0x02, 0x00, 0x04, 0x00,
|
|
testUnknownRequiredType, 0x00,
|
|
},
|
|
expErr: hop.ErrInvalidPayload{
|
|
Type: testUnknownRequiredType,
|
|
Violation: hop.RequiredViolation,
|
|
FinalHop: true,
|
|
},
|
|
},
|
|
{
|
|
name: "required type after included hop id",
|
|
payload: []byte{
|
|
0x02, 0x00, 0x04, 0x00, 0x06, 0x08, 0x01, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00,
|
|
testUnknownRequiredType, 0x00,
|
|
},
|
|
expErr: hop.ErrInvalidPayload{
|
|
Type: testUnknownRequiredType,
|
|
Violation: hop.RequiredViolation,
|
|
FinalHop: false,
|
|
},
|
|
},
|
|
{
|
|
name: "required type zero final hop",
|
|
payload: []byte{0x00, 0x00, 0x02, 0x00, 0x04, 0x00},
|
|
expErr: hop.ErrInvalidPayload{
|
|
Type: 0,
|
|
Violation: hop.RequiredViolation,
|
|
FinalHop: true,
|
|
},
|
|
},
|
|
{
|
|
name: "required type zero final hop zero sid",
|
|
payload: []byte{0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06, 0x08,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
},
|
|
expErr: hop.ErrInvalidPayload{
|
|
Type: record.NextHopOnionType,
|
|
Violation: hop.IncludedViolation,
|
|
FinalHop: true,
|
|
},
|
|
},
|
|
{
|
|
name: "required type zero intermediate hop",
|
|
payload: []byte{0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x06, 0x08,
|
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
},
|
|
expErr: hop.ErrInvalidPayload{
|
|
Type: 0,
|
|
Violation: hop.RequiredViolation,
|
|
FinalHop: false,
|
|
},
|
|
},
|
|
{
|
|
name: "required type in custom range",
|
|
payload: []byte{0x02, 0x00, 0x04, 0x00,
|
|
0xfe, 0x00, 0x01, 0x00, 0x00, 0x02, 0x10, 0x11,
|
|
},
|
|
expCustomRecords: map[uint64][]byte{
|
|
65536: {0x10, 0x11},
|
|
},
|
|
},
|
|
{
|
|
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: "intermediate hop with amp",
|
|
payload: []byte{
|
|
// amount
|
|
0x02, 0x00,
|
|
// cltv
|
|
0x04, 0x00,
|
|
// next hop id
|
|
0x06, 0x08,
|
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
// amp
|
|
0x0e, 0x41,
|
|
// amp.root_share
|
|
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
|
|
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
|
|
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
|
|
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
|
|
// amp.set_id
|
|
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
|
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
|
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
|
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
|
// amp.child_index
|
|
0x09,
|
|
},
|
|
expErr: hop.ErrInvalidPayload{
|
|
Type: record.AMPOnionType,
|
|
Violation: hop.IncludedViolation,
|
|
FinalHop: false,
|
|
},
|
|
},
|
|
{
|
|
name: "intermediate hop with encrypted data",
|
|
payload: []byte{
|
|
// amount
|
|
0x02, 0x00,
|
|
// cltv
|
|
0x04, 0x00,
|
|
// encrypted data
|
|
0x0a, 0x03, 0x03, 0x02, 0x01,
|
|
},
|
|
shouldHaveEncData: true,
|
|
},
|
|
{
|
|
name: "intermediate hop with blinding point",
|
|
payload: append([]byte{
|
|
// amount
|
|
0x02, 0x00,
|
|
// cltv
|
|
0x04, 0x00,
|
|
// blinding point (type / length)
|
|
0x0c, 0x21,
|
|
},
|
|
// blinding point (value)
|
|
testPubKey.SerializeCompressed()...,
|
|
),
|
|
shouldHaveBlinding: true,
|
|
},
|
|
{
|
|
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,
|
|
},
|
|
{
|
|
name: "final hop with amp",
|
|
payload: []byte{
|
|
// amount
|
|
0x02, 0x00,
|
|
// cltv
|
|
0x04, 0x00,
|
|
// amp
|
|
0x0e, 0x41,
|
|
// amp.root_share
|
|
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
|
|
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
|
|
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
|
|
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
|
|
// amp.set_id
|
|
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
|
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
|
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
|
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
|
// amp.child_index
|
|
0x09,
|
|
},
|
|
shouldHaveAMP: true,
|
|
},
|
|
{
|
|
name: "final hop with metadata",
|
|
payload: []byte{
|
|
// amount
|
|
0x02, 0x00,
|
|
// cltv
|
|
0x04, 0x00,
|
|
// metadata
|
|
0x10, 0x03, 0x01, 0x02, 0x03,
|
|
},
|
|
shouldHaveMetadata: true,
|
|
},
|
|
{
|
|
name: "final hop with total amount",
|
|
payload: []byte{
|
|
// amount
|
|
0x02, 0x00,
|
|
// cltv
|
|
0x04, 0x00,
|
|
// total amount
|
|
0x12, 0x01, 0x01,
|
|
},
|
|
shouldHaveTotalAmt: true,
|
|
},
|
|
}
|
|
|
|
// TestDecodeHopPayloadRecordValidation asserts that parsing the payloads in the
|
|
// tests yields the expected errors depending on whether the proper fields were
|
|
// included or omitted.
|
|
func TestDecodeHopPayloadRecordValidation(t *testing.T) {
|
|
for _, test := range decodePayloadTests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
testDecodeHopPayloadValidation(t, test)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testDecodeHopPayloadValidation(t *testing.T, test decodePayloadTest) {
|
|
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,
|
|
}
|
|
|
|
testRootShare = [32]byte{
|
|
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
|
|
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
|
|
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
|
|
0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12, 0x12,
|
|
}
|
|
testSetID = [32]byte{
|
|
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
|
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
|
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
|
0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x13,
|
|
}
|
|
testEncData = []byte{3, 2, 1}
|
|
testMetadata = []byte{1, 2, 3}
|
|
testChildIndex = uint32(9)
|
|
)
|
|
|
|
p, err := hop.NewPayloadFromReader(bytes.NewReader(test.payload))
|
|
if !reflect.DeepEqual(test.expErr, err) {
|
|
t.Fatalf("expected error mismatch, want: %v, got: %v",
|
|
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")
|
|
}
|
|
|
|
if test.shouldHaveAMP {
|
|
if p.AMP == nil {
|
|
t.Fatalf("payload should have AMP record")
|
|
}
|
|
require.Equal(t, testRootShare, p.AMP.RootShare())
|
|
require.Equal(t, testSetID, p.AMP.SetID())
|
|
require.Equal(t, testChildIndex, p.AMP.ChildIndex())
|
|
} else if p.AMP != nil {
|
|
t.Fatalf("unexpected AMP payload")
|
|
}
|
|
|
|
if test.shouldHaveMetadata {
|
|
if p.Metadata() == nil {
|
|
t.Fatalf("payload should have metadata")
|
|
}
|
|
require.Equal(t, testMetadata, p.Metadata())
|
|
} else if p.Metadata() != nil {
|
|
t.Fatalf("unexpected metadata")
|
|
}
|
|
|
|
if test.shouldHaveEncData {
|
|
require.NotNil(t, p.EncryptedData(),
|
|
"payment should have encrypted data")
|
|
|
|
require.Equal(t, testEncData, p.EncryptedData())
|
|
} else {
|
|
require.Nil(t, p.EncryptedData())
|
|
}
|
|
|
|
if test.shouldHaveBlinding {
|
|
require.NotNil(t, p.BlindingPoint())
|
|
|
|
require.Equal(t, testPubKey, p.BlindingPoint())
|
|
} else {
|
|
require.Nil(t, p.BlindingPoint())
|
|
}
|
|
|
|
if test.shouldHaveTotalAmt {
|
|
require.NotZero(t, p.TotalAmtMsat())
|
|
} else {
|
|
require.Zero(t, p.TotalAmtMsat())
|
|
}
|
|
|
|
// Convert expected nil map to empty map, because we always expect an
|
|
// initiated map from the payload.
|
|
expCustomRecords := make(record.CustomSet)
|
|
if test.expCustomRecords != nil {
|
|
expCustomRecords = test.expCustomRecords
|
|
}
|
|
if !reflect.DeepEqual(expCustomRecords, p.CustomRecords()) {
|
|
t.Fatalf("invalid custom records")
|
|
}
|
|
}
|