package record import ( "bytes" "encoding/hex" "fmt" "math" "testing" "github.com/btcsuite/btcd/btcec/v2" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" ) //nolint:lll const pubkeyStr = "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619" func pubkey(t *testing.T) *btcec.PublicKey { t.Helper() nodeBytes, err := hex.DecodeString(pubkeyStr) require.NoError(t, err) nodePk, err := btcec.ParsePubKey(nodeBytes) require.NoError(t, err) return nodePk } // TestBlindedDataEncoding tests encoding and decoding of blinded data blobs. // These tests specifically cover cases where the variable length encoded // integers values have different numbers of leading zeros trimmed because // these TLVs are the first composite records with variable length tlvs // (previously, a variable length integer would take up the whole record). func TestBlindedDataEncoding(t *testing.T) { t.Parallel() tests := []struct { name string baseFee lnwire.MilliSatoshi htlcMin lnwire.MilliSatoshi features *lnwire.FeatureVector constraints bool }{ { name: "zero variable values", baseFee: 0, htlcMin: 0, }, { name: "zeros trimmed", baseFee: math.MaxUint32 / 2, htlcMin: math.MaxUint64 / 2, }, { name: "no zeros trimmed", baseFee: math.MaxUint32, htlcMin: math.MaxUint64, }, { name: "nil feature vector", features: nil, }, { name: "non-nil, but empty feature vector", features: lnwire.EmptyFeatureVector(), }, { name: "populated feature vector", features: lnwire.NewFeatureVector( lnwire.NewRawFeatureVector(lnwire.AMPOptional), lnwire.Features, ), }, { name: "no payment constraints", constraints: true, }, } for _, testCase := range tests { testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() // Create a standard set of blinded route data, using // the values from our test case for the variable // length encoded values. channelID := lnwire.NewShortChanIDFromInt(1) info := PaymentRelayInfo{ FeeRate: 2, CltvExpiryDelta: 3, BaseFee: testCase.baseFee, } var constraints *PaymentConstraints if testCase.constraints { constraints = &PaymentConstraints{ MaxCltvExpiry: 4, HtlcMinimumMsat: testCase.htlcMin, } } encodedData := NewNonFinalBlindedRouteData( channelID, pubkey(t), info, constraints, testCase.features, ) encoded, err := EncodeBlindedRouteData(encodedData) require.NoError(t, err) b := bytes.NewBuffer(encoded) decodedData, err := DecodeBlindedRouteData(b) require.NoError(t, err) require.Equal(t, encodedData, decodedData) }) } } // TestBlindedDataFinalHopEncoding tests the encoding and decoding of a blinded // data blob intended for the final hop of a blinded path where only the pathID // will potentially be set. func TestBlindedDataFinalHopEncoding(t *testing.T) { tests := []struct { name string pathID []byte constraints bool }{ { name: "with path ID", pathID: []byte{1, 2, 3, 4, 5, 6}, }, { name: "with no path ID", pathID: nil, }, { name: "with path ID and constraints", pathID: []byte{1, 2, 3, 4, 5, 6}, constraints: true, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() var constraints *PaymentConstraints if test.constraints { constraints = &PaymentConstraints{ MaxCltvExpiry: 4, HtlcMinimumMsat: 5, } } encodedData := NewFinalHopBlindedRouteData( constraints, test.pathID, ) encoded, err := EncodeBlindedRouteData(encodedData) require.NoError(t, err) b := bytes.NewBuffer(encoded) decodedData, err := DecodeBlindedRouteData(b) require.NoError(t, err) require.Equal(t, encodedData, decodedData) }) } } // TestDummyHopBlindedDataEncoding tests the encoding and decoding of a blinded // data blob intended for hops preceding a dummy hop in a blinded path. These // hops provide the reader with a signal that the next hop may be a dummy hop. func TestDummyHopBlindedDataEncoding(t *testing.T) { t.Parallel() priv, err := btcec.NewPrivateKey() require.NoError(t, err) info := PaymentRelayInfo{ FeeRate: 2, CltvExpiryDelta: 3, BaseFee: 30, } constraints := PaymentConstraints{ MaxCltvExpiry: 4, HtlcMinimumMsat: 100, } routeData := NewDummyHopRouteData(priv.PubKey(), info, constraints) encoded, err := EncodeBlindedRouteData(routeData) require.NoError(t, err) // Assert the size of an average dummy hop payload in case we need to // update this constant in future. require.Len(t, encoded, AverageDummyHopPayloadSize) b := bytes.NewBuffer(encoded) decodedData, err := DecodeBlindedRouteData(b) require.NoError(t, err) require.Equal(t, routeData, decodedData) } // TestBlindedRouteDataPadding tests the PadBy method of BlindedRouteData. func TestBlindedRouteDataPadding(t *testing.T) { newBlindedRouteData := func() *BlindedRouteData { channelID := lnwire.NewShortChanIDFromInt(1) info := PaymentRelayInfo{ FeeRate: 2, CltvExpiryDelta: 3, BaseFee: 30, } constraints := &PaymentConstraints{ MaxCltvExpiry: 4, HtlcMinimumMsat: 100, } return NewNonFinalBlindedRouteData( channelID, pubkey(t), info, constraints, nil, ) } tests := []struct { name string paddingSize int expectedSizeIncrease uint64 }{ { // Calling PadBy with an n value of 0 in the case where // there is not yet a padding field will result in a // zero length TLV entry being added. This will add 2 // bytes for the type and length fields. name: "no extra padding", expectedSizeIncrease: 2, }, { name: "small padding (length " + "field of 1 byte)", paddingSize: 200, expectedSizeIncrease: 202, }, { name: "medium padding (length field " + "of 3 bytes)", paddingSize: 256, expectedSizeIncrease: 260, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { data := newBlindedRouteData() prePaddingEncoding, err := EncodeBlindedRouteData(data) require.NoError(t, err) data.PadBy(test.paddingSize) postPaddingEncoding, err := EncodeBlindedRouteData(data) require.NoError(t, err) require.EqualValues( t, test.expectedSizeIncrease, len(postPaddingEncoding)- len(prePaddingEncoding), ) }) } } // TestBlindedRouteVectors tests encoding/decoding of the test vectors for // blinded route data provided in the specification. // //nolint:lll func TestBlindingSpecTestVectors(t *testing.T) { nextBlindingOverrideStr, err := hex.DecodeString("031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f") require.NoError(t, err) nextBlindingOverride, err := btcec.ParsePubKey(nextBlindingOverrideStr) require.NoError(t, err) tests := []struct { encoded string expectedPaymentData *BlindedRouteData expectedPadding int }{ { encoded: "011a0000000000000000000000000000000000000000000000000000020800000000000006c10a0800240000009627100c06000b69e505dc0e00fd023103123456", expectedPaymentData: NewNonFinalBlindedRouteData( lnwire.ShortChannelID{ BlockHeight: 0, TxIndex: 0, TxPosition: 1729, }, nil, PaymentRelayInfo{ CltvExpiryDelta: 36, FeeRate: 150, BaseFee: 10000, }, &PaymentConstraints{ MaxCltvExpiry: 748005, HtlcMinimumMsat: 1500, }, lnwire.NewFeatureVector( lnwire.NewRawFeatureVector(), lnwire.Features, ), ), expectedPadding: 26, }, { encoded: "020800000000000004510821031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f0a0800300000006401f40c06000b69c105dc0e00", expectedPaymentData: NewNonFinalBlindedRouteData( lnwire.ShortChannelID{ TxPosition: 1105, }, nextBlindingOverride, PaymentRelayInfo{ CltvExpiryDelta: 48, FeeRate: 100, BaseFee: 500, }, &PaymentConstraints{ MaxCltvExpiry: 747969, HtlcMinimumMsat: 1500, }, lnwire.NewFeatureVector( lnwire.NewRawFeatureVector(), lnwire.Features, ), ), }, } for i, test := range tests { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { route, err := hex.DecodeString(test.encoded) require.NoError(t, err) buff := bytes.NewBuffer(route) decodedRoute, err := DecodeBlindedRouteData(buff) require.NoError(t, err) if test.expectedPadding != 0 { test.expectedPaymentData.PadBy( test.expectedPadding, ) } require.Equal( t, test.expectedPaymentData, decodedRoute, ) }) } }