package blindedpath import ( "bytes" "encoding/hex" "fmt" "math/rand" "reflect" "testing" "testing/quick" "github.com/btcsuite/btcd/btcec/v2" sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/record" "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/tlv" "github.com/stretchr/testify/require" ) var ( pubkeyBytes, _ = hex.DecodeString( "598ec453728e0ffe0ae2f5e174243cf58f2" + "a3f2c83d2457b43036db568b11093", ) pubKeyY = new(btcec.FieldVal) _ = pubKeyY.SetByteSlice(pubkeyBytes) pubkey = btcec.NewPublicKey(new(btcec.FieldVal).SetInt(4), pubKeyY) ) // TestApplyBlindedPathPolicyBuffer tests blinded policy adjustments. func TestApplyBlindedPathPolicyBuffer(t *testing.T) { tests := []struct { name string policyIn *BlindedHopPolicy expectedOut *BlindedHopPolicy incMultiplier float64 decMultiplier float64 expectedError string }{ { name: "invalid increase multiplier", incMultiplier: 0, expectedError: "blinded path policy increase " + "multiplier must be greater than or equal to 1", }, { name: "decrease multiplier too small", incMultiplier: 1, decMultiplier: -1, expectedError: "blinded path policy decrease " + "multiplier must be in the range [0;1]", }, { name: "decrease multiplier too big", incMultiplier: 1, decMultiplier: 2, expectedError: "blinded path policy decrease " + "multiplier must be in the range [0;1]", }, { name: "no change", incMultiplier: 1, decMultiplier: 1, policyIn: &BlindedHopPolicy{ CLTVExpiryDelta: 1, MinHTLCMsat: 2, MaxHTLCMsat: 3, BaseFee: 4, FeeRate: 5, }, expectedOut: &BlindedHopPolicy{ CLTVExpiryDelta: 1, MinHTLCMsat: 2, MaxHTLCMsat: 3, BaseFee: 4, FeeRate: 5, }, }, { name: "buffer up by 100% and down by 50%", incMultiplier: 2, decMultiplier: 0.5, policyIn: &BlindedHopPolicy{ CLTVExpiryDelta: 10, MinHTLCMsat: 20, MaxHTLCMsat: 300, BaseFee: 40, FeeRate: 50, }, expectedOut: &BlindedHopPolicy{ CLTVExpiryDelta: 20, MinHTLCMsat: 40, MaxHTLCMsat: 150, BaseFee: 80, FeeRate: 100, }, }, { name: "new HTLC minimum larger than OG " + "maximum", incMultiplier: 2, decMultiplier: 1, policyIn: &BlindedHopPolicy{ CLTVExpiryDelta: 10, MinHTLCMsat: 20, MaxHTLCMsat: 30, BaseFee: 40, FeeRate: 50, }, expectedOut: &BlindedHopPolicy{ CLTVExpiryDelta: 20, MinHTLCMsat: 20, MaxHTLCMsat: 30, BaseFee: 80, FeeRate: 100, }, }, { name: "new HTLC maximum smaller than OG " + "minimum", incMultiplier: 1, decMultiplier: 0.5, policyIn: &BlindedHopPolicy{ CLTVExpiryDelta: 10, MinHTLCMsat: 20, MaxHTLCMsat: 30, BaseFee: 40, FeeRate: 50, }, expectedOut: &BlindedHopPolicy{ CLTVExpiryDelta: 10, MinHTLCMsat: 20, MaxHTLCMsat: 30, BaseFee: 40, FeeRate: 50, }, }, { name: "new HTLC minimum and maximums are not " + "compatible", incMultiplier: 2, decMultiplier: 0.5, policyIn: &BlindedHopPolicy{ CLTVExpiryDelta: 10, MinHTLCMsat: 30, MaxHTLCMsat: 100, BaseFee: 40, FeeRate: 50, }, expectedOut: &BlindedHopPolicy{ CLTVExpiryDelta: 20, MinHTLCMsat: 30, MaxHTLCMsat: 100, BaseFee: 80, FeeRate: 100, }, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() bufferedPolicy, err := AddPolicyBuffer( test.policyIn, test.incMultiplier, test.decMultiplier, ) if test.expectedError != "" { require.ErrorContains( t, err, test.expectedError, ) return } require.Equal(t, test.expectedOut, bufferedPolicy) }) } } // TestBlindedPathAccumulatedPolicyCalc tests the logic for calculating the // accumulated routing policies of a blinded route against an example mentioned // in the spec document: // https://github.com/lightning/bolts/blob/master/proposals/route-blinding.md func TestBlindedPathAccumulatedPolicyCalc(t *testing.T) { t.Parallel() // In the spec example, the blinded route is: // Carol -> Bob -> Alice // And Alice chooses the following buffered policy for both the C->B // and B->A edges. nodePolicy := &record.PaymentRelayInfo{ FeeRate: 500, BaseFee: 100, CltvExpiryDelta: 144, } hopPolicies := []*record.PaymentRelayInfo{ nodePolicy, nodePolicy, } // Alice's minimum final expiry delta is chosen to be 12. aliceMinFinalExpDelta := uint16(12) totalBase, totalRate, totalCLTVDelta := calcBlindedPathPolicies( hopPolicies, aliceMinFinalExpDelta, ) require.Equal(t, lnwire.MilliSatoshi(201), totalBase) require.EqualValues(t, 1001, totalRate) require.EqualValues(t, 300, totalCLTVDelta) } // TestPadBlindedHopInfo asserts that the padding of blinded hop data is done // correctly and that it takes the expected number of iterations. func TestPadBlindedHopInfo(t *testing.T) { tests := []struct { name string expectedIterations int expectedFinalSize int // We will use the PathID field of BlindedRouteData to set an // initial payload size. The ints in this list represent the // size of each PathID. pathIDs []int // existingPadding is a map from entry index (based on the // pathIDs set) to the number of pre-existing padding bytes to // add. existingPadding map[int]int // prePad is true if all the hop payloads should be pre-padded // with a zero length TLV Padding field. prePad bool // minPayloadSize can be used to set the minimum number of bytes // that the resulting records should be. minPayloadSize int }{ { // If there is only one entry, then no padding is // expected. name: "single entry", expectedIterations: 1, pathIDs: []int{10}, // The final size will be 12 since the path ID is 10 // bytes, and it will be prefixed by type and value // bytes. expectedFinalSize: 12, }, { // Same as the above example but with a minimum final // size specified. name: "single entry with min size", expectedIterations: 2, pathIDs: []int{10}, minPayloadSize: 500, expectedFinalSize: 504, }, { // All the payloads are the same size from the get go // meaning that no padding is expected. name: "all start equal", expectedIterations: 1, pathIDs: []int{10, 10, 10}, // The final size will be 12 since the path ID is 10 // bytes, and it will be prefixed by type and value // bytes. expectedFinalSize: 12, }, { // If the blobs differ by 1 byte it will take 4 // iterations: // 1) padding of 1 is added to entry 2 which will // increase its size by 3 bytes since padding does // not yet exist for it. // 2) Now entry 1 will be short 2 bytes. It will be // padded by 2 bytes but again since it is a new // padding field, 4 bytes are added. // 3) Finally, entry 2 is padded by 1 extra. Since it // already does have a padding field, this does end // up adding only 1 extra byte. // 4) The fourth iteration determines that all are now // the same size. name: "differ by 1 - no pre-padding", expectedIterations: 4, pathIDs: []int{4, 3}, expectedFinalSize: 10, }, { // By pre-padding the payloads with a zero byte padding, // we can reduce the number of iterations quite a bit. name: "differ by 1 - with pre-padding", expectedIterations: 2, pathIDs: []int{4, 3}, expectedFinalSize: 8, prePad: true, }, { name: "existing padding and diff of 1", expectedIterations: 2, pathIDs: []int{10, 11}, // By adding some existing padding, the type and length // field for the padding are already accounted for in // the first iteration, and so we only expect two // iterations to get the payloads to match size here: // one for adding a single extra byte to the smaller // payload and another for confirming the sizes match. existingPadding: map[int]int{0: 1, 1: 1}, expectedFinalSize: 16, }, { // In this test, we test a BigSize bucket shift. We do // this by setting the initial path ID's of both entries // to a 0 size which means the total encoding of those // will be 2 bytes (to encode the type and length). Then // for the initial padding, we let the first entry be // 253 bytes long which is just long enough to be in // the second BigSize bucket which uses 3 bytes to // encode the value length. We make the second entry // 252 bytes which still puts it in the first bucket // which uses 1 byte for the length. The difference in // overall packet size will be 3 bytes (the first entry // has 2 more length bytes and 1 more value byte). So // the function will try to pad the second entry by 3 // bytes (iteration 1). This will however result in the // second entry shifting to the second BigSize bucket // meaning it will gain an additional 2 bytes for the // new length encoding meaning that overall it gains 5 // bytes in size. This will result in another iteration // which will result in padding the first entry with an // extra 2 bytes to meet the second entry's new size // (iteration 2). One more iteration (3) is then done // to confirm that all entries are now the same size. name: "big size bucket shift", expectedIterations: 3, // We make the path IDs large enough so that pathIDs: []int{0, 0}, existingPadding: map[int]int{0: 253, 1: 252}, expectedFinalSize: 261, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() // If the test includes existing padding, then make sure // that the number of existing padding entries is equal // to the number of PathID entries. if test.existingPadding != nil { require.Len(t, test.existingPadding, len(test.pathIDs)) } hopDataSet := make([]*hopData, len(test.pathIDs)) for i, l := range test.pathIDs { pathID := tlv.SomeRecordT( tlv.NewPrimitiveRecord[tlv.TlvType6]( make([]byte, l), ), ) data := &record.BlindedRouteData{ PathID: pathID, } if test.existingPadding != nil { //nolint:lll padding := tlv.SomeRecordT( tlv.NewPrimitiveRecord[tlv.TlvType1]( make([]byte, test.existingPadding[i]), ), ) data.Padding = padding } hopDataSet[i] = &hopData{data: data} } hopInfo, stats, err := padHopInfo( hopDataSet, test.prePad, test.minPayloadSize, ) require.NoError(t, err) require.Equal(t, test.expectedIterations, stats.numIterations) require.Equal(t, test.expectedFinalSize, stats.finalPaddedSize) // We expect all resulting blobs to be the same size. for _, info := range hopInfo { require.Len( t, info.PlainText, test.expectedFinalSize, ) } }) } } // TestPadBlindedHopInfoBlackBox tests the padHopInfo function via the // quick.Check testing function. It generates a random set of hopData and // asserts that the resulting padded set always has the same encoded length. func TestPadBlindedHopInfoBlackBox(t *testing.T) { fn := func(data hopDataList) bool { resultList, _, err := padHopInfo(data, true, 0) require.NoError(t, err) // There should be a resulting sphinx.HopInfo struct for each // hopData passed to the padHopInfo function. if len(resultList) != len(data) { return false } // There is nothing left to check if input set was empty to // start with. if len(data) == 0 { return true } // Now, assert that the encoded size of each item is the same. // Get the size of the first item as a base point. payloadSize := len(resultList[0].PlainText) // All the other entries should have the same encoded size. for i := 1; i < len(resultList); i++ { if len(resultList[i].PlainText) != payloadSize { return false } } return true } require.NoError(t, quick.Check(fn, nil)) } type hopDataList []*hopData // Generate returns a random instance of the hopDataList type. // // NOTE: this is part of the quick.Generate interface. func (h hopDataList) Generate(rand *rand.Rand, size int) reflect.Value { data := make(hopDataList, rand.Intn(size)) for i := 0; i < len(data); i++ { data[i] = &hopData{ data: genBlindedRouteData(rand), nodeID: pubkey, } } return reflect.ValueOf(data) } // A compile-time check to ensure that hopDataList implements the // quick.Generator interface. var _ quick.Generator = (*hopDataList)(nil) // sometimesDo calls the given function with a 50% probability. func sometimesDo(fn func(), rand *rand.Rand) { if rand.Intn(1) == 0 { return } fn() } // genBlindedRouteData generates a random record.BlindedRouteData object. func genBlindedRouteData(rand *rand.Rand) *record.BlindedRouteData { var data record.BlindedRouteData sometimesDo(func() { data.Padding = tlv.SomeRecordT( tlv.NewPrimitiveRecord[tlv.TlvType1]( make([]byte, rand.Intn(1000000)), ), ) }, rand) sometimesDo(func() { data.ShortChannelID = tlv.SomeRecordT( tlv.NewRecordT[tlv.TlvType2](lnwire.ShortChannelID{ BlockHeight: rand.Uint32(), TxIndex: rand.Uint32(), TxPosition: uint16(rand.Uint32()), }), ) }, rand) sometimesDo(func() { data.NextNodeID = tlv.SomeRecordT( tlv.NewPrimitiveRecord[tlv.TlvType4](pubkey), ) }, rand) sometimesDo(func() { data.PathID = tlv.SomeRecordT( tlv.NewPrimitiveRecord[tlv.TlvType6]( make([]byte, rand.Intn(100)), ), ) }, rand) sometimesDo(func() { data.NextBlindingOverride = tlv.SomeRecordT( tlv.NewPrimitiveRecord[tlv.TlvType8](pubkey), ) }, rand) sometimesDo(func() { data.RelayInfo = tlv.SomeRecordT( tlv.NewRecordT[tlv.TlvType10](record.PaymentRelayInfo{ CltvExpiryDelta: uint16(rand.Uint32()), FeeRate: rand.Uint32(), BaseFee: lnwire.MilliSatoshi( rand.Uint32(), ), }), ) }, rand) sometimesDo(func() { data.Constraints = tlv.SomeRecordT( tlv.NewRecordT[tlv.TlvType12](record.PaymentConstraints{ MaxCltvExpiry: rand.Uint32(), HtlcMinimumMsat: lnwire.MilliSatoshi( rand.Uint32(), ), }), ) }, rand) return &data } // TestBuildBlindedPath tests the logic for constructing a blinded path against // an example mentioned in this spec document: // https://github.com/lightning/bolts/blob/master/proposals/route-blinding.md // This example does not use any dummy hops. func TestBuildBlindedPath(t *testing.T) { // Alice chooses the following path to herself for blinded path // construction: // Carol -> Bob -> Alice. // Let's construct the corresponding route.Route for this which will be // returned from the `FindRoutes` config callback. var ( privC, pkC = btcec.PrivKeyFromBytes([]byte{1}) privB, pkB = btcec.PrivKeyFromBytes([]byte{2}) privA, pkA = btcec.PrivKeyFromBytes([]byte{3}) carol = route.NewVertex(pkC) bob = route.NewVertex(pkB) alice = route.NewVertex(pkA) chanCB = uint64(1) chanBA = uint64(2) ) realRoute := &route.Route{ SourcePubKey: carol, Hops: []*route.Hop{ { PubKeyBytes: bob, ChannelID: chanCB, }, { PubKeyBytes: alice, ChannelID: chanBA, }, }, } realPolicies := map[uint64]*models.ChannelEdgePolicy{ chanCB: { ChannelID: chanCB, ToNode: bob, }, chanBA: { ChannelID: chanBA, ToNode: alice, }, } paths, err := BuildBlindedPaymentPaths(&BuildBlindedPathCfg{ FindRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route, error) { return []*route.Route{realRoute}, nil }, FetchChannelEdgesByID: func(chanID uint64) ( *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { return nil, realPolicies[chanID], nil, nil }, BestHeight: func() (uint32, error) { return 1000, nil }, // In the spec example, all the policies get replaced with // the same static values. AddPolicyBuffer: func(_ *BlindedHopPolicy) ( *BlindedHopPolicy, error) { return &BlindedHopPolicy{ FeeRate: 500, BaseFee: 100, CLTVExpiryDelta: 144, MinHTLCMsat: 1000, MaxHTLCMsat: lnwire.MaxMilliSatoshi, }, nil }, PathID: []byte{1, 2, 3}, ValueMsat: 1000, MinFinalCLTVExpiryDelta: 12, BlocksUntilExpiry: 200, }) require.NoError(t, err) require.Len(t, paths, 1) path := paths[0] // Check that all the accumulated policy values are correct. require.EqualValues(t, 201, path.FeeBaseMsat) require.EqualValues(t, 1001, path.FeeRate) require.EqualValues(t, 300, path.CltvExpiryDelta) require.EqualValues(t, 1000, path.HTLCMinMsat) require.EqualValues(t, lnwire.MaxMilliSatoshi, path.HTLCMaxMsat) // Now we check the hops. require.Len(t, path.Hops, 3) // Assert that all the encrypted recipient blobs have been padded such // that they are all the same size. require.Len(t, path.Hops[0].CipherText, len(path.Hops[1].CipherText)) require.Len(t, path.Hops[1].CipherText, len(path.Hops[2].CipherText)) // The first hop, should have the real pub key of the introduction // node: Carol. hop := path.Hops[0] require.True(t, hop.BlindedNodePub.IsEqual(pkC)) // As Carol, let's decode the hop data and assert that all expected // values have been included. var ( blindingPoint = path.FirstEphemeralBlindingPoint data *record.BlindedRouteData ) // Check that Carol's info is correct. data, blindingPoint = decryptAndDecodeHopData( t, privC, blindingPoint, hop.CipherText, ) require.Equal( t, lnwire.NewShortChanIDFromInt(chanCB), data.ShortChannelID.UnwrapOrFail(t).Val, ) require.Equal(t, record.PaymentRelayInfo{ CltvExpiryDelta: 144, FeeRate: 500, BaseFee: 100, }, data.RelayInfo.UnwrapOrFail(t).Val) require.Equal(t, record.PaymentConstraints{ MaxCltvExpiry: 1500, HtlcMinimumMsat: 1000, }, data.Constraints.UnwrapOrFail(t).Val) // Check that all Bob's info is correct. hop = path.Hops[1] data, blindingPoint = decryptAndDecodeHopData( t, privB, blindingPoint, hop.CipherText, ) require.Equal( t, lnwire.NewShortChanIDFromInt(chanBA), data.ShortChannelID.UnwrapOrFail(t).Val, ) require.Equal(t, record.PaymentRelayInfo{ CltvExpiryDelta: 144, FeeRate: 500, BaseFee: 100, }, data.RelayInfo.UnwrapOrFail(t).Val) require.Equal(t, record.PaymentConstraints{ MaxCltvExpiry: 1356, HtlcMinimumMsat: 1000, }, data.Constraints.UnwrapOrFail(t).Val) // Check that all Alice's info is correct. hop = path.Hops[2] data, _ = decryptAndDecodeHopData( t, privA, blindingPoint, hop.CipherText, ) require.True(t, data.ShortChannelID.IsNone()) require.True(t, data.RelayInfo.IsNone()) require.Equal(t, record.PaymentConstraints{ MaxCltvExpiry: 1212, HtlcMinimumMsat: 1000, }, data.Constraints.UnwrapOrFail(t).Val) require.Equal(t, []byte{1, 2, 3}, data.PathID.UnwrapOrFail(t).Val) } // TestBuildBlindedPathWithDummyHops tests the construction of a blinded path // which includes dummy hops. func TestBuildBlindedPathWithDummyHops(t *testing.T) { // Alice chooses the following path to herself for blinded path // construction: // Carol -> Bob -> Alice. // Let's construct the corresponding route.Route for this which will be // returned from the `FindRoutes` config callback. var ( privC, pkC = btcec.PrivKeyFromBytes([]byte{1}) privB, pkB = btcec.PrivKeyFromBytes([]byte{2}) privA, pkA = btcec.PrivKeyFromBytes([]byte{3}) carol = route.NewVertex(pkC) bob = route.NewVertex(pkB) alice = route.NewVertex(pkA) chanCB = uint64(1) chanBA = uint64(2) ) realRoute := &route.Route{ SourcePubKey: carol, Hops: []*route.Hop{ { PubKeyBytes: bob, ChannelID: chanCB, }, { PubKeyBytes: alice, ChannelID: chanBA, }, }, } realPolicies := map[uint64]*models.ChannelEdgePolicy{ chanCB: { ChannelID: chanCB, ToNode: bob, }, chanBA: { ChannelID: chanBA, ToNode: alice, }, } paths, err := BuildBlindedPaymentPaths(&BuildBlindedPathCfg{ FindRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route, error) { return []*route.Route{realRoute}, nil }, FetchChannelEdgesByID: func(chanID uint64) ( *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { policy, ok := realPolicies[chanID] if !ok { return nil, nil, nil, fmt.Errorf("edge not found") } return nil, policy, nil, nil }, BestHeight: func() (uint32, error) { return 1000, nil }, // In the spec example, all the policies get replaced with // the same static values. AddPolicyBuffer: func(_ *BlindedHopPolicy) ( *BlindedHopPolicy, error) { return &BlindedHopPolicy{ FeeRate: 500, BaseFee: 100, CLTVExpiryDelta: 144, MinHTLCMsat: 1000, MaxHTLCMsat: lnwire.MaxMilliSatoshi, }, nil }, PathID: []byte{1, 2, 3}, ValueMsat: 1000, MinFinalCLTVExpiryDelta: 12, BlocksUntilExpiry: 200, // By setting the minimum number of hops to 4, we force 2 dummy // hops to be added to the real route. MinNumHops: 4, DefaultDummyHopPolicy: &BlindedHopPolicy{ CLTVExpiryDelta: 50, FeeRate: 100, BaseFee: 100, MinHTLCMsat: 1000, MaxHTLCMsat: lnwire.MaxMilliSatoshi, }, }) require.NoError(t, err) require.Len(t, paths, 1) path := paths[0] // Check that all the accumulated policy values are correct. require.EqualValues(t, 403, path.FeeBaseMsat) require.EqualValues(t, 2003, path.FeeRate) require.EqualValues(t, 588, path.CltvExpiryDelta) require.EqualValues(t, 1000, path.HTLCMinMsat) require.EqualValues(t, lnwire.MaxMilliSatoshi, path.HTLCMaxMsat) // Now we check the hops. require.Len(t, path.Hops, 5) // Assert that all the encrypted recipient blobs have been padded such // that they are all the same size. require.Len(t, path.Hops[0].CipherText, len(path.Hops[1].CipherText)) require.Len(t, path.Hops[1].CipherText, len(path.Hops[2].CipherText)) require.Len(t, path.Hops[2].CipherText, len(path.Hops[3].CipherText)) require.Len(t, path.Hops[3].CipherText, len(path.Hops[4].CipherText)) // The first hop, should have the real pub key of the introduction // node: Carol. hop := path.Hops[0] require.True(t, hop.BlindedNodePub.IsEqual(pkC)) // As Carol, let's decode the hop data and assert that all expected // values have been included. var ( blindingPoint = path.FirstEphemeralBlindingPoint data *record.BlindedRouteData ) // Check that Carol's info is correct. data, blindingPoint = decryptAndDecodeHopData( t, privC, blindingPoint, hop.CipherText, ) require.Equal( t, lnwire.NewShortChanIDFromInt(chanCB), data.ShortChannelID.UnwrapOrFail(t).Val, ) require.Equal(t, record.PaymentRelayInfo{ CltvExpiryDelta: 144, FeeRate: 500, BaseFee: 100, }, data.RelayInfo.UnwrapOrFail(t).Val) require.Equal(t, record.PaymentConstraints{ MaxCltvExpiry: 1788, HtlcMinimumMsat: 1000, }, data.Constraints.UnwrapOrFail(t).Val) // Check that all Bob's info is correct. hop = path.Hops[1] data, blindingPoint = decryptAndDecodeHopData( t, privB, blindingPoint, hop.CipherText, ) require.Equal( t, lnwire.NewShortChanIDFromInt(chanBA), data.ShortChannelID.UnwrapOrFail(t).Val, ) require.Equal(t, record.PaymentRelayInfo{ CltvExpiryDelta: 144, FeeRate: 500, BaseFee: 100, }, data.RelayInfo.UnwrapOrFail(t).Val) require.Equal(t, record.PaymentConstraints{ MaxCltvExpiry: 1644, HtlcMinimumMsat: 1000, }, data.Constraints.UnwrapOrFail(t).Val) // Check that all Alice's info is correct. The payload should contain // a next_node_id field that is equal to Alice's public key. This // indicates to Alice that she should continue peeling the onion. hop = path.Hops[2] data, blindingPoint = decryptAndDecodeHopData( t, privA, blindingPoint, hop.CipherText, ) require.True(t, data.ShortChannelID.IsNone()) require.True(t, data.RelayInfo.IsSome()) require.True(t, data.Constraints.IsSome()) require.Equal(t, pkA, data.NextNodeID.UnwrapOrFail(t).Val) // Alice should be able to decrypt the next payload with her private // key. This next payload is yet another dummy hop. hop = path.Hops[3] data, blindingPoint = decryptAndDecodeHopData( t, privA, blindingPoint, hop.CipherText, ) require.True(t, data.ShortChannelID.IsNone()) require.True(t, data.RelayInfo.IsSome()) require.True(t, data.Constraints.IsSome()) require.Equal(t, pkA, data.NextNodeID.UnwrapOrFail(t).Val) // Unwrapping one more time should reveal the final hop info for Alice. hop = path.Hops[4] data, _ = decryptAndDecodeHopData( t, privA, blindingPoint, hop.CipherText, ) require.True(t, data.ShortChannelID.IsNone()) require.True(t, data.RelayInfo.IsNone()) require.Equal(t, record.PaymentConstraints{ MaxCltvExpiry: 1212, HtlcMinimumMsat: 1000, }, data.Constraints.UnwrapOrFail(t).Val) require.Equal(t, []byte{1, 2, 3}, data.PathID.UnwrapOrFail(t).Val) // Demonstrate that BuildBlindedPaymentPaths continues to use any // functioning paths even if some routes cant be used to build a blinded // path. We do this by forcing FetchChannelEdgesByID to error out for // the first 2 calls. FindRoutes returns 3 routes and so by the end, we // still get 1 valid path. var errCount int paths, err = BuildBlindedPaymentPaths(&BuildBlindedPathCfg{ FindRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route, error) { return []*route.Route{realRoute, realRoute, realRoute}, nil }, FetchChannelEdgesByID: func(chanID uint64) ( *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { // Force the call to error for the first 2 channels. if errCount < 2 { errCount++ return nil, nil, nil, fmt.Errorf("edge not found") } policy, ok := realPolicies[chanID] if !ok { return nil, nil, nil, fmt.Errorf("edge not found") } return nil, policy, nil, nil }, BestHeight: func() (uint32, error) { return 1000, nil }, // In the spec example, all the policies get replaced with // the same static values. AddPolicyBuffer: func(_ *BlindedHopPolicy) ( *BlindedHopPolicy, error) { return &BlindedHopPolicy{ FeeRate: 500, BaseFee: 100, CLTVExpiryDelta: 144, MinHTLCMsat: 1000, MaxHTLCMsat: lnwire.MaxMilliSatoshi, }, nil }, PathID: []byte{1, 2, 3}, ValueMsat: 1000, MinFinalCLTVExpiryDelta: 12, BlocksUntilExpiry: 200, // By setting the minimum number of hops to 4, we force 2 dummy // hops to be added to the real route. MinNumHops: 4, DefaultDummyHopPolicy: &BlindedHopPolicy{ CLTVExpiryDelta: 50, FeeRate: 100, BaseFee: 100, MinHTLCMsat: 1000, MaxHTLCMsat: lnwire.MaxMilliSatoshi, }, }) require.NoError(t, err) require.Len(t, paths, 1) } // TestSingleHopBlindedPath tests that blinded path construction is done // correctly for the case where the destination node is also the introduction // node. func TestSingleHopBlindedPath(t *testing.T) { var ( _, pkC = btcec.PrivKeyFromBytes([]byte{1}) carol = route.NewVertex(pkC) ) realRoute := &route.Route{ SourcePubKey: carol, // No hops since Carol is both the introduction node and the // final destination node. Hops: []*route.Hop{}, } paths, err := BuildBlindedPaymentPaths(&BuildBlindedPathCfg{ FindRoutes: func(_ lnwire.MilliSatoshi) ([]*route.Route, error) { return []*route.Route{realRoute}, nil }, BestHeight: func() (uint32, error) { return 1000, nil }, PathID: []byte{1, 2, 3}, ValueMsat: 1000, MinFinalCLTVExpiryDelta: 12, BlocksUntilExpiry: 200, }) require.NoError(t, err) require.Len(t, paths, 1) path := paths[0] // Check that all the accumulated policy values are correct. Since this // is a unique case where the destination node is also the introduction // node, the accumulated fee and HTLC values should be zero and the // CLTV expiry delta should be equal to Carol's MinFinalCLTVExpiryDelta. require.EqualValues(t, 0, path.FeeBaseMsat) require.EqualValues(t, 0, path.FeeRate) require.EqualValues(t, 0, path.HTLCMinMsat) require.EqualValues(t, 0, path.HTLCMaxMsat) require.EqualValues(t, 12, path.CltvExpiryDelta) } func decryptAndDecodeHopData(t *testing.T, priv *btcec.PrivateKey, ephem *btcec.PublicKey, cipherText []byte) (*record.BlindedRouteData, *btcec.PublicKey) { router := sphinx.NewRouter( &keychain.PrivKeyECDH{PrivKey: priv}, nil, ) decrypted, err := router.DecryptBlindedHopData(ephem, cipherText) require.NoError(t, err) buf := bytes.NewBuffer(decrypted) routeData, err := record.DecodeBlindedRouteData(buf) require.NoError(t, err) nextEphem, err := router.NextEphemeral(ephem) require.NoError(t, err) return routeData, nextEphem }