| 
 | 1 | +package itest  | 
 | 2 | + | 
 | 3 | +import (  | 
 | 4 | +	"bytes"  | 
 | 5 | +	"fmt"  | 
 | 6 | +	"time"  | 
 | 7 | + | 
 | 8 | +	"github.com/btcsuite/btcd/btcec/v2"  | 
 | 9 | +	sphinx "github.com/lightningnetwork/lightning-onion"  | 
 | 10 | +	"github.com/lightningnetwork/lnd/lnrpc"  | 
 | 11 | +	"github.com/lightningnetwork/lnd/lntest"  | 
 | 12 | +	"github.com/lightningnetwork/lnd/lnwire"  | 
 | 13 | +	"github.com/lightningnetwork/lnd/record"  | 
 | 14 | +	"github.com/stretchr/testify/require"  | 
 | 15 | +)  | 
 | 16 | + | 
 | 17 | +// testOnionMessage tests forwarding of onion messages.  | 
 | 18 | +func testOnionMessageForwarding(ht *lntest.HarnessTest) {  | 
 | 19 | +	// Spin up a three node because we will need a three-hop network for  | 
 | 20 | +	// this test.  | 
 | 21 | +	alice := ht.NewNodeWithCoins("Alice", nil)  | 
 | 22 | +	bob := ht.NewNodeWithCoins("Bob", nil)  | 
 | 23 | +	carol := ht.NewNode("Carol", nil)  | 
 | 24 | + | 
 | 25 | +	// Create a session key for the blinded path.  | 
 | 26 | +	blindingKey, err := btcec.NewPrivateKey()  | 
 | 27 | +	require.NoError(ht.T, err)  | 
 | 28 | + | 
 | 29 | +	sessionKey, err := btcec.NewPrivateKey()  | 
 | 30 | +	require.NoError(ht.T, err)  | 
 | 31 | + | 
 | 32 | +	// Connect nodes before channel opening so that they can share gossip.  | 
 | 33 | +	ht.ConnectNodesPerm(alice, bob)  | 
 | 34 | +	ht.ConnectNodesPerm(bob, carol)  | 
 | 35 | + | 
 | 36 | +	// Open channels: Alice --- Bob --- Carol and wait for each node to  | 
 | 37 | +	// sync the network graph.  | 
 | 38 | +	aliceBobChanPoint := ht.OpenChannel(alice, bob, lntest.OpenChannelParams{  | 
 | 39 | +		Amt: 500_000,  | 
 | 40 | +	})  | 
 | 41 | +	ht.AssertNumChannelUpdates(carol, aliceBobChanPoint, 2)  | 
 | 42 | + | 
 | 43 | +	bobCarolChanPoint := ht.OpenChannel(bob, carol, lntest.OpenChannelParams{  | 
 | 44 | +		Amt: 500_000,  | 
 | 45 | +	})  | 
 | 46 | +	ht.AssertNumChannelUpdates(alice, bobCarolChanPoint, 2)  | 
 | 47 | + | 
 | 48 | +	// Create a blinded route  | 
 | 49 | + | 
 | 50 | +	// Create a set of 2 blinded hops for our path.  | 
 | 51 | +	hopsToBlind := make([]*sphinx.HopInfo, 2)  | 
 | 52 | + | 
 | 53 | +	// Our path is: Alice -> Bob -> Carol  | 
 | 54 | +	// So Bob needs to receive the public Key of Carol.  | 
 | 55 | + | 
 | 56 | +	carolPubKey, err := btcec.ParsePubKey(carol.PubKey[:])  | 
 | 57 | +	require.NoError(ht.T, err)  | 
 | 58 | +	data0 := record.NewNonFinalBlindedRouteDataOnionMessage(  | 
 | 59 | +		carolPubKey, nil, nil, nil,  | 
 | 60 | +	)  | 
 | 61 | +	encoded0, err := record.EncodeBlindedRouteData(data0)  | 
 | 62 | +	require.NoError(ht.T, err)  | 
 | 63 | + | 
 | 64 | +	data1 := &record.BlindedRouteData{}  | 
 | 65 | +	encoded1, err := record.EncodeBlindedRouteData(data1)  | 
 | 66 | +	require.NoError(ht.T, err)  | 
 | 67 | + | 
 | 68 | +	bobPubKey, err := btcec.ParsePubKey(bob.PubKey[:])  | 
 | 69 | +	require.NoError(ht.T, err)  | 
 | 70 | + | 
 | 71 | +	// The first hop is for Bob. This will be blinded at a later stage.  | 
 | 72 | +	hopsToBlind[0] = &sphinx.HopInfo{  | 
 | 73 | +		NodePub:   bobPubKey,  | 
 | 74 | +		PlainText: encoded0,  | 
 | 75 | +	}  | 
 | 76 | +	// The second hop is to Carol.  | 
 | 77 | +	hopsToBlind[1] = &sphinx.HopInfo{  | 
 | 78 | +		NodePub:   carolPubKey,  | 
 | 79 | +		PlainText: encoded1,  | 
 | 80 | +	}  | 
 | 81 | + | 
 | 82 | +	blindedPath, err := sphinx.BuildBlindedPath(blindingKey, hopsToBlind)  | 
 | 83 | +	require.NoError(ht.T, err)  | 
 | 84 | + | 
 | 85 | +	finalHopPayload := &lnwire.FinalHopPayload{  | 
 | 86 | +		TLVType: lnwire.InvoiceRequestNamespaceType,  | 
 | 87 | +		Value:   []byte{1, 2, 3},  | 
 | 88 | +	}  | 
 | 89 | + | 
 | 90 | +	// Convert that blinded path to a sphinx path and add a final payload.  | 
 | 91 | +	sphinxPath, err := blindedToSphinx(  | 
 | 92 | +		blindedPath.Path, nil, nil, []*lnwire.FinalHopPayload{  | 
 | 93 | +			finalHopPayload,  | 
 | 94 | +		},  | 
 | 95 | +	)  | 
 | 96 | +	require.NoError(ht.T, err)  | 
 | 97 | + | 
 | 98 | +	// Create an onion packet with no associated data.  | 
 | 99 | +	onionPacket, err := sphinx.NewOnionPacket(  | 
 | 100 | +		sphinxPath, sessionKey, nil, sphinx.DeterministicPacketFiller,  | 
 | 101 | +		sphinx.MaxRoutingPayloadSize, sphinx.MaxOnionMessagePayloadSize,  | 
 | 102 | +	)  | 
 | 103 | + | 
 | 104 | +	buf := new(bytes.Buffer)  | 
 | 105 | +	err = onionPacket.Encode(buf)  | 
 | 106 | +	require.NoError(ht.T, err, "encode onion packet")  | 
 | 107 | + | 
 | 108 | +	// Subscribe Carol to onion messages before we send any, so that we  | 
 | 109 | +	// don't miss any.  | 
 | 110 | +	msgClient, cancel := carol.RPC.SubscribeOnionMessages()  | 
 | 111 | +	defer cancel()  | 
 | 112 | + | 
 | 113 | +	// Create a channel to receive onion messages on.  | 
 | 114 | +	messages := make(chan *lnrpc.OnionMessageUpdate)  | 
 | 115 | +	go func() {  | 
 | 116 | +		for {  | 
 | 117 | +			// If we fail to receive, just exit. The test should  | 
 | 118 | +			// fail elsewhere if it doesn't get a message that it  | 
 | 119 | +			// was expecting.  | 
 | 120 | +			msg, err := msgClient.Recv()  | 
 | 121 | +			if err != nil {  | 
 | 122 | +				return  | 
 | 123 | +			}  | 
 | 124 | + | 
 | 125 | +			// Deliver the message into our channel or exit if the  | 
 | 126 | +			// test is shutting down.  | 
 | 127 | +			select {  | 
 | 128 | +			case messages <- msg:  | 
 | 129 | +			case <-ht.Context().Done():  | 
 | 130 | +				return  | 
 | 131 | +			}  | 
 | 132 | +		}  | 
 | 133 | +	}()  | 
 | 134 | + | 
 | 135 | +	blindingPoint := blindingKey.PubKey().SerializeCompressed()  | 
 | 136 | + | 
 | 137 | +	// Send it from Alice to Bob.  | 
 | 138 | +	aliceMsg := &lnrpc.SendOnionMessageRequest{  | 
 | 139 | +		Peer:          bob.PubKey[:],  | 
 | 140 | +		BlindingPoint: blindingPoint,  | 
 | 141 | +		Onion:         buf.Bytes(),  | 
 | 142 | +	}  | 
 | 143 | +	alice.RPC.SendOnionMessage(aliceMsg)  | 
 | 144 | + | 
 | 145 | +	// Wait for Carol to receive the message.  | 
 | 146 | +	select {  | 
 | 147 | +	case msg := <-messages:  | 
 | 148 | +		// Check our type and data and (sanity) check the peer we got  | 
 | 149 | +		// it from.  | 
 | 150 | +		require.Equal(ht, bob.PubKey[:], msg.Peer, "msg peer wrong")  | 
 | 151 | +		require.NotEmpty(ht, msg.CustomRecords)  | 
 | 152 | +		require.NotNil(ht, msg.CustomRecords[uint64(lnwire.InvoiceRequestNamespaceType)])  | 
 | 153 | +		require.Equal(ht, msg.CustomRecords[uint64(lnwire.InvoiceRequestNamespaceType)], []byte{1, 2, 3})  | 
 | 154 | + | 
 | 155 | +	case <-time.After(lntest.DefaultTimeout):  | 
 | 156 | +		ht.Fatalf("carol did not receive onion message: %v", aliceMsg)  | 
 | 157 | +	}  | 
 | 158 | + | 
 | 159 | +	ht.CloseChannel(alice, aliceBobChanPoint)  | 
 | 160 | +	ht.CloseChannel(bob, bobCarolChanPoint)  | 
 | 161 | +}  | 
 | 162 | + | 
 | 163 | +// blindedToSphinx converts the blinded path provided to a sphinx path that can  | 
 | 164 | +// be wrapped up in an onion, encoding the TLV payload for each hop along the  | 
 | 165 | +// way.  | 
 | 166 | +func blindedToSphinx(blindedRoute *sphinx.BlindedPath,  | 
 | 167 | +	extraHops []*lnwire.BlindedHop, replyPath *lnwire.ReplyPath,  | 
 | 168 | +	finalPayloads []*lnwire.FinalHopPayload) (  | 
 | 169 | +	*sphinx.PaymentPath, error) {  | 
 | 170 | + | 
 | 171 | +	var (  | 
 | 172 | +		sphinxPath sphinx.PaymentPath  | 
 | 173 | + | 
 | 174 | +		ourHopCount   = len(blindedRoute.BlindedHops)  | 
 | 175 | +		extraHopCount = len(extraHops)  | 
 | 176 | +	)  | 
 | 177 | + | 
 | 178 | +	// Fill in the blinded node id and encrypted data for all hops. This  | 
 | 179 | +	// requirement differs from blinded hops used for payments, where we  | 
 | 180 | +	// don't use the blinded introduction node id. However, since onion  | 
 | 181 | +	// messages are fully blinded by default, we use the blinded  | 
 | 182 | +	// introduction node id.  | 
 | 183 | +	for i := 0; i < ourHopCount; i++ {  | 
 | 184 | +		// Create an onion message payload with the encrypted data for  | 
 | 185 | +		// this hop.  | 
 | 186 | +		payload := &lnwire.OnionMessagePayload{  | 
 | 187 | +			EncryptedData: blindedRoute.BlindedHops[i].CipherText,  | 
 | 188 | +		}  | 
 | 189 | + | 
 | 190 | +		// If we're on the final hop and there are no extra hops to add  | 
 | 191 | +		// onto our path, include the tlvs intended for the final hop  | 
 | 192 | +		// and the reply path (if provided).  | 
 | 193 | +		if i == ourHopCount-1 && extraHopCount == 0 {  | 
 | 194 | +			payload.FinalHopPayloads = finalPayloads  | 
 | 195 | +			payload.ReplyPath = replyPath  | 
 | 196 | +		}  | 
 | 197 | + | 
 | 198 | +		// Encode the tlv stream for inclusion in our message.  | 
 | 199 | +		hop, err := createSphinxHop(  | 
 | 200 | +			*blindedRoute.BlindedHops[i].BlindedNodePub, payload,  | 
 | 201 | +		)  | 
 | 202 | +		if err != nil {  | 
 | 203 | +			return nil, fmt.Errorf("sphinx hop %v: %w", i, err)  | 
 | 204 | +		}  | 
 | 205 | +		sphinxPath[i] = *hop  | 
 | 206 | +	}  | 
 | 207 | + | 
 | 208 | +	// If we don't have any more hops to append to our path, just return  | 
 | 209 | +	// it as-is here.  | 
 | 210 | +	if extraHopCount == 0 {  | 
 | 211 | +		return &sphinxPath, nil  | 
 | 212 | +	}  | 
 | 213 | + | 
 | 214 | +	for i := 0; i < extraHopCount; i++ {  | 
 | 215 | +		payload := &lnwire.OnionMessagePayload{  | 
 | 216 | +			EncryptedData: extraHops[i].EncryptedData,  | 
 | 217 | +		}  | 
 | 218 | + | 
 | 219 | +		// If we're on the last hop, add our optional final payload  | 
 | 220 | +		// and reply path.  | 
 | 221 | +		if i == extraHopCount-1 {  | 
 | 222 | +			payload.FinalHopPayloads = finalPayloads  | 
 | 223 | +			payload.ReplyPath = replyPath  | 
 | 224 | +		}  | 
 | 225 | + | 
 | 226 | +		hop, err := createSphinxHop(  | 
 | 227 | +			*extraHops[i].BlindedNodeID, payload,  | 
 | 228 | +		)  | 
 | 229 | +		if err != nil {  | 
 | 230 | +			return nil, fmt.Errorf("sphinx hop %v: %w", i, err)  | 
 | 231 | +		}  | 
 | 232 | + | 
 | 233 | +		// We need to offset our index in the sphinx path by the  | 
 | 234 | +		// number of hops that we added in the loop above.  | 
 | 235 | +		sphinxIndex := i + ourHopCount  | 
 | 236 | +		sphinxPath[sphinxIndex] = *hop  | 
 | 237 | +	}  | 
 | 238 | + | 
 | 239 | +	return &sphinxPath, nil  | 
 | 240 | +}  | 
 | 241 | + | 
 | 242 | +// createSphinxHop encodes an onion message payload and produces a sphinx  | 
 | 243 | +// onion hop for it.  | 
 | 244 | +func createSphinxHop(nodeID btcec.PublicKey,  | 
 | 245 | +	payload *lnwire.OnionMessagePayload) (*sphinx.OnionHop, error) {  | 
 | 246 | + | 
 | 247 | +	payloadTLVs, err := payload.Encode()  | 
 | 248 | +	if err != nil {  | 
 | 249 | +		return nil, fmt.Errorf("payload: encode: %v", err)  | 
 | 250 | +	}  | 
 | 251 | + | 
 | 252 | +	return &sphinx.OnionHop{  | 
 | 253 | +		NodePub: nodeID,  | 
 | 254 | +		HopPayload: sphinx.HopPayload{  | 
 | 255 | +			Type:    sphinx.PayloadTLV,  | 
 | 256 | +			Payload: payloadTLVs,  | 
 | 257 | +		},  | 
 | 258 | +	}, nil  | 
 | 259 | +}  | 
0 commit comments