Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
218 changes: 212 additions & 6 deletions itest/rfq_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import (
"context"
"fmt"
"math"
"math/rand"
"testing"
"time"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/taproot-assets/rfq"
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
"github.com/lightningnetwork/lnd/chainreg"
Expand Down Expand Up @@ -213,6 +216,157 @@ func testRfqAssetBuyHtlcIntercept(t *harnessTest) {
require.NoError(t.t, err)
}

// testRfqAssetSellHtlcIntercept tests RFQ negotiation, HTLC interception, and
// validation between three peers. The RFQ negotiation is initiated by an asset
// sell request.
func testRfqAssetSellHtlcIntercept(t *harnessTest) {
// Initialize a new test scenario.
ts := newRfqTestScenario(t)

// Mint an asset with Alice's tapd node.
rpcAssets := MintAssetsConfirmBatch(
t.t, t.lndHarness.Miner.Client, ts.AliceTapd,
[]*mintrpc.MintAssetRequest{issuableAssets[0]},
)
mintedAssetId := rpcAssets[0].AssetGenesis.AssetId

ctxb := context.Background()
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
defer cancel()

// TODO(ffranr): Add an asset buy offer to Bob's tapd node. This will
// allow Alice to sell the newly minted asset to Bob.

// Subscribe to Alice's RFQ events stream.
aliceEventNtfns, err := ts.AliceTapd.SubscribeRfqEventNtfns(
ctxb, &rfqrpc.SubscribeRfqEventNtfnsRequest{},
)
require.NoError(t.t, err)

// Alice sends a sell order to Bob for some amount of the newly minted
// asset.
purchaseAssetAmt := uint64(200)
askAmt := uint64(42000)
sellOrderExpiry := uint64(time.Now().Add(24 * time.Hour).Unix())

_, err = ts.AliceTapd.AddAssetSellOrder(
ctxt, &rfqrpc.AddAssetSellOrderRequest{
AssetSpecifier: &rfqrpc.AssetSpecifier{
Id: &rfqrpc.AssetSpecifier_AssetId{
AssetId: mintedAssetId,
},
},
MaxAssetAmount: purchaseAssetAmt,
MinAsk: askAmt,
Expiry: sellOrderExpiry,

// Here we explicitly specify Bob as the destination
// peer for the sell order. This will prompt Alice's
// tapd node to send a request for quote message to
// Bob's node.
PeerPubKey: ts.BobLnd.PubKey[:],
},
)
require.NoError(t.t, err, "unable to upsert asset sell order")

// Wait until Alice receives an incoming sell quote accept message (sent
// from Bob) RFQ event notification.
BeforeTimeout(t.t, func() {
event, err := aliceEventNtfns.Recv()
require.NoError(t.t, err)

_, ok := event.Event.(*rfqrpc.RfqEvent_PeerAcceptedSellQuote)
require.True(t.t, ok, "unexpected event: %v", event)
}, defaultWaitTimeout)

// Alice should have received an accepted quote from Bob. This accepted
// quote can be used by Alice to make a payment to Bob.
acceptedQuotes, err := ts.AliceTapd.QueryPeerAcceptedQuotes(
ctxt, &rfqrpc.QueryPeerAcceptedQuotesRequest{},
)
require.NoError(t.t, err, "unable to query accepted quotes")
require.Len(t.t, acceptedQuotes.SellQuotes, 1)

acceptedQuote := acceptedQuotes.SellQuotes[0]

// Register to receive RFQ events from Bob's tapd node. We'll use this
// to wait for Bob to receive the HTLC with the asset transfer specific
// scid.
bobEventNtfns, err := ts.BobTapd.SubscribeRfqEventNtfns(
ctxb, &rfqrpc.SubscribeRfqEventNtfnsRequest{},
)
require.NoError(t.t, err)

// Carol generates and invoice for Alice to settle via Bob.
addInvoiceResp := ts.CarolLnd.RPC.AddInvoice(&lnrpc.Invoice{
ValueMsat: int64(askAmt),
})
invoice := ts.CarolLnd.RPC.LookupInvoice(addInvoiceResp.RHash)

// Decode the payment request to get the payment address.
payReq := ts.CarolLnd.RPC.DecodePayReq(invoice.PaymentRequest)

// We now need to construct a route for the payment from Alice to Carol.
// The route will be Alice -> Bob -> Carol. We'll add the accepted quote
// ID as a record to the custom records field of the route's first hop.
// This will allow Bob to validate the payment against the accepted
// quote.
routeBuildRequest := routerrpc.BuildRouteRequest{
AmtMsat: int64(askAmt),
HopPubkeys: [][]byte{
ts.BobLnd.PubKey[:],
ts.CarolLnd.PubKey[:],
},
PaymentAddr: payReq.PaymentAddr,
}
routeBuildResp := ts.AliceLnd.RPC.BuildRoute(&routeBuildRequest)

// Add the accepted quote ID as a record to the custom records field of
// the route's first hop.
aliceBobHop := routeBuildResp.Route.Hops[0]
if aliceBobHop.CustomRecords == nil {
aliceBobHop.CustomRecords = make(map[uint64][]byte)
}

aliceBobHop.CustomRecords[rfq.LnCustomRecordType] =
acceptedQuote.Id[:]

// Update the route with the modified first hop.
routeBuildResp.Route.Hops[0] = aliceBobHop

// Send the payment to the route.
t.Log("Alice paying invoice")
routeReq := routerrpc.SendToRouteRequest{
PaymentHash: invoice.RHash,
Route: routeBuildResp.Route,
}
sendAttempt := ts.AliceLnd.RPC.SendToRouteV2(&routeReq)
require.Equal(t.t, lnrpc.HTLCAttempt_SUCCEEDED, sendAttempt.Status)

// At this point Bob should have received a HTLC with the asset transfer
// specific scid. We'll wait for Bob to publish an accept HTLC event and
// then validate it against the accepted quote.
t.Log("Waiting for Bob to receive HTLC")
BeforeTimeout(t.t, func() {
event, err := bobEventNtfns.Recv()
require.NoError(t.t, err)

_, ok := event.Event.(*rfqrpc.RfqEvent_AcceptHtlc)
require.True(t.t, ok, "unexpected event: %v", event)
}, defaultWaitTimeout)

// Confirm that Carol receives the lightning payment from Alice via Bob.
invoice = ts.CarolLnd.RPC.LookupInvoice(addInvoiceResp.RHash)
require.Equal(t.t, invoice.State, lnrpc.Invoice_SETTLED)

// Close event notification streams.
err = aliceEventNtfns.CloseSend()
require.NoError(t.t, err)

err = bobEventNtfns.CloseSend()
require.NoError(t.t, err)
}

// newLndNode creates a new lnd node with the given name and funds its wallet
// with the specified outputs.
func newLndNode(name string, outputFunds []btcutil.Amount,
Expand Down Expand Up @@ -286,10 +440,15 @@ func newRfqTestScenario(t *harnessTest) *rfqTestScenario {
outputFunds[i] = fundAmount
}

// Generate a unique name for each new node.
aliceName := genRandomNodeName("AliceLnd")
bobName := genRandomNodeName("BobLnd")
carolName := genRandomNodeName("CarolLnd")

// Create three new nodes.
aliceLnd := newLndNode("AliceLnd", outputFunds[:], t.lndHarness)
bobLnd := newLndNode("BobLnd", outputFunds[:], t.lndHarness)
carolLnd := newLndNode("CarolLnd", outputFunds[:], t.lndHarness)
aliceLnd := newLndNode(aliceName, outputFunds[:], t.lndHarness)
bobLnd := newLndNode(bobName, outputFunds[:], t.lndHarness)
carolLnd := newLndNode(carolName, outputFunds[:], t.lndHarness)

// Now we want to wait for the nodes to catch up.
t.lndHarness.WaitForBlockchainSync(aliceLnd)
Expand Down Expand Up @@ -347,12 +506,59 @@ func newRfqTestScenario(t *harnessTest) *rfqTestScenario {

// Cleanup cleans up the test scenario.
func (s *rfqTestScenario) Cleanup() {
// Close the LND channels.
s.testHarness.lndHarness.CloseChannel(s.AliceLnd, s.AliceBobChannel)
s.testHarness.lndHarness.CloseChannel(s.BobLnd, s.BobCarolChannel)
s.testHarness.t.Log("Cleaning up test scenario")

// Stop the tapd nodes.
require.NoError(s.testHarness.t, s.AliceTapd.stop(!*noDelete))
require.NoError(s.testHarness.t, s.BobTapd.stop(!*noDelete))
require.NoError(s.testHarness.t, s.CarolTapd.stop(!*noDelete))

// Kill the LND nodes in the test harness node manager. If we don't
// perform this step the LND test harness node manager will continue to
// run the nodes in the background as "active nodes".
s.testHarness.lndHarness.KillNode(s.AliceLnd)
s.testHarness.lndHarness.KillNode(s.BobLnd)
s.testHarness.lndHarness.KillNode(s.CarolLnd)
}

// randomString generates a random string of the given length.
func randomString(randStrLen int) string {
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, randStrLen)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}

// genRandomNodeName generates a random node name by appending a random string
// to the given base name.
func genRandomNodeName(baseName string) string {
return fmt.Sprintf("%s-%s", baseName, randomString(8))
}

// BeforeTimeout executes a function in a goroutine with a timeout. It waits for
// the function to finish or for the timeout to expire, whichever happens first.
// If the function exceeds the timeout, it logs a test error.
func BeforeTimeout(t *testing.T, targetFunc func(),
timeout time.Duration) {

// Create a channel to signal when the target function has completed.
targetExecComplete := make(chan bool, 1)

// Execute the target function in a goroutine.
go func() {
targetFunc()
targetExecComplete <- true
}()

// Wait for the target function to complete or timeout.
select {
case <-targetExecComplete:
return

case <-time.After(timeout):
t.Errorf("targetFunc did not complete within timeout: %v",
timeout)
}
}
5 changes: 5 additions & 0 deletions itest/test_list_on_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ var testCases = []*testCase{
name: "rfq asset buy htlc intercept",
test: testRfqAssetBuyHtlcIntercept,
},
{
name: "rfq asset sell htlc intercept",
test: testRfqAssetSellHtlcIntercept,
},

{
name: "multi signature on all levels",
test: testMultiSignature,
Expand Down
4 changes: 4 additions & 0 deletions perms/perms.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,10 @@ var (
Entity: "rfq",
Action: "write",
}},
"/rfqrpc.Rfq/AddAssetSellOrder": {{
Entity: "rfq",
Action: "write",
}},
"/rfqrpc.Rfq/AddAssetSellOffer": {{
Entity: "rfq",
Action: "write",
Expand Down
Loading