@@ -4,10 +4,13 @@ import (
44 "context"
55 "fmt"
66 "math"
7+ "math/rand"
8+ "testing"
79 "time"
810
911 "github.com/btcsuite/btcd/btcutil"
1012 "github.com/btcsuite/btcd/wire"
13+ "github.com/lightninglabs/taproot-assets/rfq"
1114 "github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
1215 "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
1316 "github.com/lightningnetwork/lnd/chainreg"
@@ -213,6 +216,157 @@ func testRfqAssetBuyHtlcIntercept(t *harnessTest) {
213216 require .NoError (t .t , err )
214217}
215218
219+ // testRfqAssetSellHtlcIntercept tests RFQ negotiation, HTLC interception, and
220+ // validation between three peers. The RFQ negotiation is initiated by an asset
221+ // sell request.
222+ func testRfqAssetSellHtlcIntercept (t * harnessTest ) {
223+ // Initialize a new test scenario.
224+ ts := newRfqTestScenario (t )
225+
226+ // Mint an asset with Alice's tapd node.
227+ rpcAssets := MintAssetsConfirmBatch (
228+ t .t , t .lndHarness .Miner .Client , ts .AliceTapd ,
229+ []* mintrpc.MintAssetRequest {issuableAssets [0 ]},
230+ )
231+ mintedAssetId := rpcAssets [0 ].AssetGenesis .AssetId
232+
233+ ctxb := context .Background ()
234+ ctxt , cancel := context .WithTimeout (ctxb , defaultWaitTimeout )
235+ defer cancel ()
236+
237+ // TODO(ffranr): Add an asset buy offer to Bob's tapd node. This will
238+ // allow Alice to sell the newly minted asset to Bob.
239+
240+ // Subscribe to Alice's RFQ events stream.
241+ aliceEventNtfns , err := ts .AliceTapd .SubscribeRfqEventNtfns (
242+ ctxb , & rfqrpc.SubscribeRfqEventNtfnsRequest {},
243+ )
244+ require .NoError (t .t , err )
245+
246+ // Alice sends a sell order to Bob for some amount of the newly minted
247+ // asset.
248+ purchaseAssetAmt := uint64 (200 )
249+ askAmt := uint64 (42000 )
250+ sellOrderExpiry := uint64 (time .Now ().Add (24 * time .Hour ).Unix ())
251+
252+ _ , err = ts .AliceTapd .AddAssetSellOrder (
253+ ctxt , & rfqrpc.AddAssetSellOrderRequest {
254+ AssetSpecifier : & rfqrpc.AssetSpecifier {
255+ Id : & rfqrpc.AssetSpecifier_AssetId {
256+ AssetId : mintedAssetId ,
257+ },
258+ },
259+ MaxAssetAmount : purchaseAssetAmt ,
260+ MinAsk : askAmt ,
261+ Expiry : sellOrderExpiry ,
262+
263+ // Here we explicitly specify Bob as the destination
264+ // peer for the sell order. This will prompt Alice's
265+ // tapd node to send a request for quote message to
266+ // Bob's node.
267+ PeerPubKey : ts .BobLnd .PubKey [:],
268+ },
269+ )
270+ require .NoError (t .t , err , "unable to upsert asset sell order" )
271+
272+ // Wait until Alice receives an incoming sell quote accept message (sent
273+ // from Bob) RFQ event notification.
274+ BeforeTimeout (t .t , func () {
275+ event , err := aliceEventNtfns .Recv ()
276+ require .NoError (t .t , err )
277+
278+ _ , ok := event .Event .(* rfqrpc.RfqEvent_PeerAcceptedSellQuote )
279+ require .True (t .t , ok , "unexpected event: %v" , event )
280+ }, defaultWaitTimeout )
281+
282+ // Alice should have received an accepted quote from Bob. This accepted
283+ // quote can be used by Alice to make a payment to Bob.
284+ acceptedQuotes , err := ts .AliceTapd .QueryPeerAcceptedQuotes (
285+ ctxt , & rfqrpc.QueryPeerAcceptedQuotesRequest {},
286+ )
287+ require .NoError (t .t , err , "unable to query accepted quotes" )
288+ require .Len (t .t , acceptedQuotes .SellQuotes , 1 )
289+
290+ acceptedQuote := acceptedQuotes .SellQuotes [0 ]
291+
292+ // Register to receive RFQ events from Bob's tapd node. We'll use this
293+ // to wait for Bob to receive the HTLC with the asset transfer specific
294+ // scid.
295+ bobEventNtfns , err := ts .BobTapd .SubscribeRfqEventNtfns (
296+ ctxb , & rfqrpc.SubscribeRfqEventNtfnsRequest {},
297+ )
298+ require .NoError (t .t , err )
299+
300+ // Carol generates and invoice for Alice to settle via Bob.
301+ addInvoiceResp := ts .CarolLnd .RPC .AddInvoice (& lnrpc.Invoice {
302+ ValueMsat : int64 (askAmt ),
303+ })
304+ invoice := ts .CarolLnd .RPC .LookupInvoice (addInvoiceResp .RHash )
305+
306+ // Decode the payment request to get the payment address.
307+ payReq := ts .CarolLnd .RPC .DecodePayReq (invoice .PaymentRequest )
308+
309+ // We now need to construct a route for the payment from Alice to Carol.
310+ // The route will be Alice -> Bob -> Carol. We'll add the accepted quote
311+ // ID as a record to the custom records field of the route's first hop.
312+ // This will allow Bob to validate the payment against the accepted
313+ // quote.
314+ routeBuildRequest := routerrpc.BuildRouteRequest {
315+ AmtMsat : int64 (askAmt ),
316+ HopPubkeys : [][]byte {
317+ ts .BobLnd .PubKey [:],
318+ ts .CarolLnd .PubKey [:],
319+ },
320+ PaymentAddr : payReq .PaymentAddr ,
321+ }
322+ routeBuildResp := ts .AliceLnd .RPC .BuildRoute (& routeBuildRequest )
323+
324+ // Add the accepted quote ID as a record to the custom records field of
325+ // the route's first hop.
326+ aliceBobHop := routeBuildResp .Route .Hops [0 ]
327+ if aliceBobHop .CustomRecords == nil {
328+ aliceBobHop .CustomRecords = make (map [uint64 ][]byte )
329+ }
330+
331+ aliceBobHop .CustomRecords [rfq .LnCustomRecordType ] =
332+ acceptedQuote .Id [:]
333+
334+ // Update the route with the modified first hop.
335+ routeBuildResp .Route .Hops [0 ] = aliceBobHop
336+
337+ // Send the payment to the route.
338+ t .Log ("Alice paying invoice" )
339+ routeReq := routerrpc.SendToRouteRequest {
340+ PaymentHash : invoice .RHash ,
341+ Route : routeBuildResp .Route ,
342+ }
343+ sendAttempt := ts .AliceLnd .RPC .SendToRouteV2 (& routeReq )
344+ require .Equal (t .t , lnrpc .HTLCAttempt_SUCCEEDED , sendAttempt .Status )
345+
346+ // At this point Bob should have received a HTLC with the asset transfer
347+ // specific scid. We'll wait for Bob to publish an accept HTLC event and
348+ // then validate it against the accepted quote.
349+ t .Log ("Waiting for Bob to receive HTLC" )
350+ BeforeTimeout (t .t , func () {
351+ event , err := bobEventNtfns .Recv ()
352+ require .NoError (t .t , err )
353+
354+ _ , ok := event .Event .(* rfqrpc.RfqEvent_AcceptHtlc )
355+ require .True (t .t , ok , "unexpected event: %v" , event )
356+ }, defaultWaitTimeout )
357+
358+ // Confirm that Carol receives the lightning payment from Alice via Bob.
359+ invoice = ts .CarolLnd .RPC .LookupInvoice (addInvoiceResp .RHash )
360+ require .Equal (t .t , invoice .State , lnrpc .Invoice_SETTLED )
361+
362+ // Close event notification streams.
363+ err = aliceEventNtfns .CloseSend ()
364+ require .NoError (t .t , err )
365+
366+ err = bobEventNtfns .CloseSend ()
367+ require .NoError (t .t , err )
368+ }
369+
216370// newLndNode creates a new lnd node with the given name and funds its wallet
217371// with the specified outputs.
218372func newLndNode (name string , outputFunds []btcutil.Amount ,
@@ -286,10 +440,15 @@ func newRfqTestScenario(t *harnessTest) *rfqTestScenario {
286440 outputFunds [i ] = fundAmount
287441 }
288442
443+ // Generate a unique name for each new node.
444+ aliceName := genRandomNodeName ("AliceLnd" )
445+ bobName := genRandomNodeName ("BobLnd" )
446+ carolName := genRandomNodeName ("CarolLnd" )
447+
289448 // Create three new nodes.
290- aliceLnd := newLndNode ("AliceLnd" , outputFunds [:], t .lndHarness )
291- bobLnd := newLndNode ("BobLnd" , outputFunds [:], t .lndHarness )
292- carolLnd := newLndNode ("CarolLnd" , outputFunds [:], t .lndHarness )
449+ aliceLnd := newLndNode (aliceName , outputFunds [:], t .lndHarness )
450+ bobLnd := newLndNode (bobName , outputFunds [:], t .lndHarness )
451+ carolLnd := newLndNode (carolName , outputFunds [:], t .lndHarness )
293452
294453 // Now we want to wait for the nodes to catch up.
295454 t .lndHarness .WaitForBlockchainSync (aliceLnd )
@@ -347,12 +506,59 @@ func newRfqTestScenario(t *harnessTest) *rfqTestScenario {
347506
348507// Cleanup cleans up the test scenario.
349508func (s * rfqTestScenario ) Cleanup () {
350- // Close the LND channels.
351- s .testHarness .lndHarness .CloseChannel (s .AliceLnd , s .AliceBobChannel )
352- s .testHarness .lndHarness .CloseChannel (s .BobLnd , s .BobCarolChannel )
509+ s .testHarness .t .Log ("Cleaning up test scenario" )
353510
354511 // Stop the tapd nodes.
355512 require .NoError (s .testHarness .t , s .AliceTapd .stop (! * noDelete ))
356513 require .NoError (s .testHarness .t , s .BobTapd .stop (! * noDelete ))
357514 require .NoError (s .testHarness .t , s .CarolTapd .stop (! * noDelete ))
515+
516+ // Kill the LND nodes in the test harness node manager. If we don't
517+ // perform this step the LND test harness node manager will continue to
518+ // run the nodes in the background as "active nodes".
519+ s .testHarness .lndHarness .KillNode (s .AliceLnd )
520+ s .testHarness .lndHarness .KillNode (s .BobLnd )
521+ s .testHarness .lndHarness .KillNode (s .CarolLnd )
522+ }
523+
524+ // randomString generates a random string of the given length.
525+ func randomString (randStrLen int ) string {
526+ const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
527+ b := make ([]byte , randStrLen )
528+ for i := range b {
529+ b [i ] = letters [rand .Intn (len (letters ))]
530+ }
531+ return string (b )
532+ }
533+
534+ // genRandomNodeName generates a random node name by appending a random string
535+ // to the given base name.
536+ func genRandomNodeName (baseName string ) string {
537+ return fmt .Sprintf ("%s-%s" , baseName , randomString (8 ))
538+ }
539+
540+ // BeforeTimeout executes a function in a goroutine with a timeout. It waits for
541+ // the function to finish or for the timeout to expire, whichever happens first.
542+ // If the function exceeds the timeout, it logs a test error.
543+ func BeforeTimeout (t * testing.T , targetFunc func (),
544+ timeout time.Duration ) {
545+
546+ // Create a channel to signal when the target function has completed.
547+ targetExecComplete := make (chan bool , 1 )
548+
549+ // Execute the target function in a goroutine.
550+ go func () {
551+ targetFunc ()
552+ targetExecComplete <- true
553+ }()
554+
555+ // Wait for the target function to complete or timeout.
556+ select {
557+ case <- targetExecComplete :
558+ return
559+
560+ case <- time .After (timeout ):
561+ t .Errorf ("targetFunc did not complete within timeout: %v" ,
562+ timeout )
563+ }
358564}
0 commit comments